Skip to content
Innovialabs

Higher-Order Functions: map, filter and reduce Explained

Basics4 min

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 reduce callback 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 map only for side effects (logging, sending requests) — use forEach or a loop.

When a plain loop is better

SituationBetter choice
You need to stop early (break)loop, or find / some / every
Sequential await calls one after anotherfor...of with await
Complex state with several accumulatorsloop
Performance-critical code on huge arraysloop, after measuring
Simple transform, filter or summap / 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 return inside a callback with curly braces, which produces undefined values.
  • 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], because map also passes the index as the radix.

FAQ

They can have some overhead, but in most application code the difference does not matter. Optimize only where profiling shows a real bottleneck.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.