Higher-Order Functions: map, filter and reduce Explained
Basics4 minInnovialabs editorial team
In short
A higher-order function takes or returns another function; map transforms each element, filter keeps the ones that pass a test, and reduce folds a collection into one value.
The short answer
A higher-order function is a function that takes another function as an argument or returns one. The three most common ones work on collections:
map— applies a function to every element and returns a new collection of the same length.filter— keeps only the elements for which a function returns true.reduce— combines all elements into a single value: a sum, an object, a maximum.
They replace typical loops with a short description of what you want instead of how to iterate.
Functions are values
In JavaScript and Python, a function is an ordinary value. You can store it in a variable, pass it to another function and return it.
const double = (x) => x * 2;
const apply = (fn, value) => fn(value);
apply(double, 5); // 10
def double(x):
return x * 2
def apply(fn, value):
return fn(value)
apply(double, 5) # 10
map, filter and reduce are exactly such functions: the loop is built in, and you pass only the step.
From loop to map, filter, reduce
Task: from a list of orders, take the paid ones and calculate the total amount.
With a loop:
let total = 0;
for (const order of orders) {
if (order.paid) {
total += order.amount;
}
}
With higher-order functions:
const total = orders
.filter((o) => o.paid)
.map((o) => o.amount)
.reduce((sum, a) => sum + a, 0);
The same in Python:
from functools import reduce
paid = filter(lambda o: o["paid"], orders)
amounts = map(lambda o: o["amount"], paid)
total = reduce(lambda s, a: s + a, amounts, 0)
In Python, idiomatic code usually prefers comprehensions and built-ins:
total = sum(o["amount"] for o in orders if o["paid"])
Note that in Python 3 map and filter return lazy iterators, not lists. Wrap them in list() if you need to reuse the result.
How reduce works
reduce takes a function with two arguments — the accumulator and the current element — and an initial value.
const counts = ["a", "b", "a"].reduce((acc, letter) => {
acc[letter] = (acc[letter] ?? 0) + 1;
return acc;
}, {});
// { a: 2, b: 1 }
Always pass the initial value. Without it, JavaScript uses the first element as the start and throws a TypeError on an empty array.
Chaining
Chains read top to bottom like a recipe: filter, transform, aggregate. A few practical rules:
- Put filter before map when possible, so you transform fewer elements.
- Keep callbacks pure: no side effects, no changes to outside variables. Then each step is easy to test separately.
- Give complex callbacks names:
.filter(isActiveUser)reads better than a three-line arrow function. - Remember that each step in a JavaScript array chain creates a new intermediate array. For typical UI data this is fine; for very large datasets in hot code, measure.
Readability limits
Higher-order functions are not automatically more readable. Warning signs:
- A
reducecallback longer than a few lines, with branches and nested objects. - Chains of five or more steps where you need comments to explain the data shape between them.
- Using
maponly for side effects (logging, sending requests) — useforEachor a loop.
When a plain loop is better
| Situation | Better choice |
|---|---|
You need to stop early (break) | loop, or find / some / every |
Sequential await calls one after another | for...of with await |
| Complex state with several accumulators | loop |
| Performance-critical code on huge arrays | loop, after measuring |
| Simple transform, filter or sum | map / filter / reduce |
forEach, map and reduce do not wait for async callbacks one by one. map with an async function returns an array of promises, which you can pass to Promise.all if parallel execution is what you want.
Common mistakes
- Forgetting
returninside a callback with curly braces, which producesundefinedvalues. - Mutating the original array inside
map. - Omitting the initial value in
reduce. - Passing a function with extra parameters directly:
["1", "2", "3"].map(parseInt)gives[1, NaN, NaN], becausemapalso passes the index as the radix.