Higher-Order Functions

โ–ถ Try It Yourself

A higher-order function is a function that either takes one or more functions as arguments, returns a function, or both. This concept โ€” borrowed from mathematics and functional programming โ€” is one of the most powerful patterns in JavaScript. You use higher-order functions every day: map, filter, reduce, forEach, sort, setTimeout, and addEventListener are all higher-order functions. In this lesson you will master the built-in higher-order array methods and learn to write your own utility functions using the same pattern.

Built-in Higher-Order Array Methods

Method Returns Use For
map(fn) New array โ€” same length, transformed values Transform every element
filter(fn) New array โ€” subset where fn returns true Keep elements matching a condition
reduce(fn, init) Single accumulated value Sum, group, build object from array
forEach(fn) undefined โ€” side effects only Execute effect for each element
find(fn) First matching element or undefined Find one item
findIndex(fn) Index of first match or -1 Find position of item
some(fn) true if any element passes Existence check
every(fn) true if all elements pass Validation check
sort(fn) Sorted array (mutates original) Custom sort order
flatMap(fn) Mapped and flattened one level Map then flatten

reduce Accumulator Patterns

Goal Initial Value Accumulator Operation
Sum numbers 0 acc + curr
Count occurrences {} acc[key] = (acc[key] ?? 0) + 1
Group by property {} Push to acc[key] array
Flatten array [] [...acc, ...curr]
Build lookup map {} acc[curr.id] = curr
Find max/min First element or -Infinity curr > acc ? curr : acc

Composing Higher-Order Functions

Pattern Example Result
Chain methods arr.filter(f).map(g).reduce(h, init) Pipeline of transformations
Compose functions compose(f, g)(x) = f(g(x)) Right-to-left function application
Pipe functions pipe(f, g)(x) = g(f(x)) Left-to-right โ€” more readable
Curry const add = a => b => a + b Partially applied functions
Note: map, filter, and reduce do not mutate the original array โ€” they return new arrays or values. sort and splice are exceptions that mutate in place. When you need a sorted copy without touching the original, use [...arr].sort(fn) or arr.slice().sort(fn) to create a copy first.
Tip: reduce is the most powerful โ€” and most misused โ€” array method. It can implement map, filter, find, and some. But using reduce when map or filter would be clearer is considered a code smell. Use the most specific tool: reduce shines for aggregations (sum, group, index) and for building objects or non-array outputs from arrays.
Warning: forEach always returns undefined โ€” it cannot be chained and it ignores any value you return from the callback. If you need a new array, use map. If you need a filtered array, use filter. Only use forEach when you want pure side effects (logging, DOM updates, calling external APIs) and genuinely do not need a return value.

Basic Example

const employees = [
    { id: 1, name: 'Alice',   dept: 'Engineering', salary: 95000, active: true  },
    { id: 2, name: 'Bob',     dept: 'Marketing',   salary: 72000, active: true  },
    { id: 3, name: 'Carol',   dept: 'Engineering', salary: 88000, active: false },
    { id: 4, name: 'Dave',    dept: 'Engineering', salary: 102000, active: true },
    { id: 5, name: 'Eve',     dept: 'Marketing',   salary: 68000, active: true  },
    { id: 6, name: 'Frank',   dept: 'HR',          salary: 61000, active: true  },
];

// โ”€โ”€ map โ€” transform every element โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const nameList = employees.map(e => e.name);
console.log(nameList);
// ['Alice', 'Bob', 'Carol', 'Dave', 'Eve', 'Frank']

const withBonus = employees.map(e => ({
    ...e,
    bonus: e.active ? Math.round(e.salary * 0.1) : 0,
}));

// โ”€โ”€ filter โ€” keep matching elements โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const activeEngineers = employees
    .filter(e => e.active)
    .filter(e => e.dept === 'Engineering');

console.log(activeEngineers.map(e => e.name));   // ['Alice', 'Dave']

// โ”€โ”€ reduce โ€” sum โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const totalPayroll = employees
    .filter(e => e.active)
    .reduce((sum, e) => sum + e.salary, 0);

console.log(`Active payroll: $${totalPayroll.toLocaleString()}`);  // $398,000

// โ”€โ”€ reduce โ€” group by department โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const byDept = employees.reduce((groups, e) => {
    const key = e.dept;
    if (!groups[key]) groups[key] = [];
    groups[key].push(e.name);
    return groups;
}, {});

console.log(byDept);
// { Engineering: ['Alice','Carol','Dave'], Marketing: ['Bob','Eve'], HR: ['Frank'] }

// โ”€โ”€ reduce โ€” build lookup map by id โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const employeeMap = employees.reduce((map, e) => {
    map[e.id] = e;
    return map;
}, {});

console.log(employeeMap[3].name);  // 'Carol' โ€” O(1) lookup

// โ”€โ”€ sort โ€” by salary descending โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const byPayDesc = [...employees].sort((a, b) => b.salary - a.salary);
console.log(byPayDesc.map(e => `${e.name}: $${e.salary}`));

// โ”€โ”€ some / every โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const hasInactive = employees.some(e => !e.active);     // true (Carol)
const allActive   = employees.every(e => e.active);     // false

// โ”€โ”€ flatMap โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const tags = [
    { title: 'JavaScript', tags: ['js', 'web'] },
    { title: 'Python',     tags: ['py', 'data', 'web'] },
];
const allTags = tags.flatMap(t => t.tags);
console.log(allTags);  // ['js', 'web', 'py', 'data', 'web']
const uniqueTags = [...new Set(allTags)];
console.log(uniqueTags);  // ['js', 'web', 'py', 'data']

// โ”€โ”€ Writing your own higher-order functions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function pipe(...fns) {
    return (value) => fns.reduce((v, fn) => fn(v), value);
}

const processPrice = pipe(
    price => price * 1.08,          // add 8% tax
    price => Math.round(price * 100) / 100,  // round to 2dp
    price => `$${price.toFixed(2)}` // format
);

console.log(processPrice(29.99));   // '$32.39'
console.log(processPrice(9.50));    // '$10.26'

How It Works

Step 1 โ€” map Transforms Without Mutation

map calls the callback once per element, collects every return value, and returns a new array of the same length. The original array is untouched. The callback receives (currentValue, index, array) โ€” most of the time you only need the first argument.

Step 2 โ€” filter Tests Each Element

filter calls the callback for each element. If it returns truthy, the element is included in the new array. If falsy, it is excluded. Chaining .filter().filter() applies conditions sequentially โ€” each filter receives the already-filtered output of the previous one.

Step 3 โ€” reduce Folds the Array into One Value

The callback receives (accumulator, currentValue, index, array). The accumulator starts as the initial value and is updated each iteration by whatever the callback returns. The final accumulator value is what reduce returns. The initial value is critical โ€” omitting it uses the first element as the accumulator, which breaks for empty arrays.

Step 4 โ€” sort’s Comparator Returns a Number

The comparator (a, b) => a.salary - b.salary returns negative (a before b), zero (equal), or positive (b before a). For descending order, flip to b.salary - a.salary. For strings, use a.name.localeCompare(b.name) โ€” never subtract strings.

Step 5 โ€” pipe Composes Functions Left to Right

pipe(...fns) returns a function that passes its input through each function in sequence, left to right. Each function receives the output of the previous one. This is a higher-order function that both takes functions as arguments AND returns a function โ€” the purest form of the pattern.

Real-World Example: Data Report Generator

// report.js

function generateReport(transactions) {
    const summary = transactions
        .filter(t => t.status === 'completed')
        .reduce((acc, t) => {
            // Running totals
            acc.totalRevenue    += t.amount;
            acc.transactionCount++;

            // Group by category
            if (!acc.byCategory[t.category]) {
                acc.byCategory[t.category] = { count: 0, total: 0 };
            }
            acc.byCategory[t.category].count++;
            acc.byCategory[t.category].total += t.amount;

            // Track largest transaction
            if (t.amount > (acc.largest?.amount ?? 0)) {
                acc.largest = t;
            }

            return acc;
        }, {
            totalRevenue: 0,
            transactionCount: 0,
            byCategory: {},
            largest: null,
        });

    summary.averageOrderValue = summary.transactionCount > 0
        ? summary.totalRevenue / summary.transactionCount
        : 0;

    // Sort categories by total descending
    summary.topCategories = Object.entries(summary.byCategory)
        .map(([name, data]) => ({ name, ...data }))
        .sort((a, b) => b.total - a.total)
        .slice(0, 3);

    return summary;
}

const transactions = [
    { id: 't1', status: 'completed', amount: 49.99,  category: 'electronics' },
    { id: 't2', status: 'refunded',  amount: 12.00,  category: 'books'       },
    { id: 't3', status: 'completed', amount: 199.00, category: 'electronics' },
    { id: 't4', status: 'completed', amount: 8.99,   category: 'books'       },
    { id: 't5', status: 'completed', amount: 34.50,  category: 'clothing'    },
];

const report = generateReport(transactions);
console.log(`Revenue:   $${report.totalRevenue.toFixed(2)}`);       // $292.48
console.log(`Avg order: $${report.averageOrderValue.toFixed(2)}`);   // $73.12
console.log('Top categories:', report.topCategories);

Common Mistakes

Mistake 1 โ€” Using forEach when map is needed

โŒ Wrong โ€” forEach returns undefined, not a new array:

const doubled = numbers.forEach(n => n * 2);
console.log(doubled);   // undefined โ€” forEach ignores return values

โœ… Correct โ€” use map when you need a new array:

const doubled = numbers.map(n => n * 2);
console.log(doubled);   // [2, 4, 6, ...]

Mistake 2 โ€” Mutating the original array in map

โŒ Wrong โ€” modifying the original element defeats the purpose of map:

const result = users.map(user => {
    user.fullName = `${user.first} ${user.last}`;  // mutates original!
    return user;
});

โœ… Correct โ€” return a new object with the spread operator:

const result = users.map(user => ({
    ...user,
    fullName: `${user.first} ${user.last}`,  // new object โ€” original untouched
}));

Mistake 3 โ€” Omitting the initial value in reduce

โŒ Wrong โ€” reduce without initial value throws on empty array:

const total = [].reduce((sum, n) => sum + n);
// TypeError: Reduce of empty array with no initial value

โœ… Correct โ€” always provide an initial value:

const total = [].reduce((sum, n) => sum + n, 0);   // safely returns 0

▶ Try It Yourself

Quick Reference

Method Returns Mutates?
map(fn) New array โ€” transformed No
filter(fn) New array โ€” subset No
reduce(fn, init) Any single value No
forEach(fn) undefined No (but callback can)
find(fn) First match or undefined No
some(fn) / every(fn) Boolean No
sort(fn) Sorted array Yes โ€” copy first with [...arr]
flatMap(fn) Mapped + flattened array No

🧠 Test Yourself

You want to calculate the total price of all items in a cart array. Which method is the best fit?





โ–ถ Try It Yourself