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 |
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.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.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
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 |