jsguides

Higher-Order Functions in JavaScript: map, filter, compose

Functions are values in JavaScript. That single fact makes one of the most powerful patterns in the language possible: higher-order functions.

A higher-order function is any function that either:

  • Takes one or more functions as arguments, or
  • Returns a function as its result

Compare that to a first-order function, which only works with primitive values like numbers and strings and does neither.

// First-order: operates only on primitives
function add(a, b) {
  return a + b;
}

add(2, 3); // 5

// Higher-order: takes a function as an argument
function operateOnNumbers(a, b, fn) {
  return fn(a, b);
}

operateOnNumbers(2, 3, add); // 5

That operateOnNumbers function does not care how add works. It just delegates to whatever function you give it. That flexibility is the entire point.

Built-in higher-order array methods

JavaScript’s arrays come with higher-order functions built in. You have probably used them already without thinking of them as HOFs.

Transforming with map

map calls a function on every element and returns a new array of the results. The original array is untouched.

const numbers = [1, 2, 3, 4];

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

map always produces a same-length array, which makes it predictable to chain. If you need to drop or keep elements rather than transform every one, reach for filter instead.

Selecting with filter

filter keeps only the elements where your callback returns true, and returns a new array.

const numbers = [1, 2, 3, 4];

const evens = numbers.filter(n => n % 2 === 0);
// [2, 4]

filter produces a subset of the original array, so the result may be shorter or even empty. The callback’s predicate doesn’t need to be complex — even a simple truthiness check like filter(Boolean) cleans up sparse arrays.

Accumulating with reduce

reduce processes every element and collapses them into a single value. You provide a callback and a starting value.

const numbers = [1, 2, 3, 4];

const sum = numbers.reduce((accumulator, n) => accumulator + n, 0);
// 10

reduce is the most flexible of the three. map and filter can both be expressed as reduce, but using them directly is cleaner.

Quick boolean checks with some and every

some returns true if at least one element passes the test. every returns true only if all elements pass.

const numbers = [1, 2, 3, 4];

numbers.some(n => n > 3);  // true; 4 satisfies it
numbers.every(n => n > 0); // true; all are positive

These four methods (map, filter, reduce, some, every) cover the vast majority of array transformation tasks. Once you start thinking in terms of “transform this array” rather than “write a loop”, your code becomes shorter and easier to reason about.

Callbacks: three ways to write them

A callback is just a function you pass to another function. JavaScript gives you three common styles.

Anonymous function expression

numbers.filter(function(n) {
  return n > 2;
});

Anonymous callbacks work fine, but when something goes wrong, your stack trace just says (anonymous function). That gets old fast in production code.

When you define a callback inline, the runtime has no name to show in error traces. This makes debugging harder over time, especially when the same pattern appears in multiple places.

Named function

function isGreaterThanTwo(n) {
  return n > 2;
}

numbers.filter(isGreaterThanTwo);

Named callbacks produce readable stack traces and are reusable. If you need the same filter logic in two places, a named function avoids duplication.

A named callback also makes your intent explicit at the point of use. Reading numbers.filter(isGreaterThanTwo) tells you what the filter does without needing to scan the callback body.

Arrow function

numbers.filter(n => n > 2);

Arrow functions are the default in modern JavaScript. They are concise and lexically bind this, which is what you want in most callback situations.

One caveat: if you are writing a callback inside a class method and that callback needs its own this, reach for a regular function expression instead. Arrow functions do not have their own this.

Closures: the mechanism behind stateful HOFs

A closure is a function that captures variables from its enclosing lexical scope. Together with higher-order functions, closures enable some genuinely useful patterns.

The canonical example is a function factory:

function makeMultiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = makeMultiplier(2);
const triple = makeMultiplier(3);

double(5); // 10
triple(5); // 15

makeMultiplier returns a function. That returned function closes over factor; it remembers that value even after makeMultiplier has already returned. Call makeMultiplier twice with different arguments and you get two independent functions that each remember their own factor.

Closures also let you keep state private:

function makeCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    decrement: () => --count,
    getCount: () => count,
  };
}

const counter = makeCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.getCount();  // 2

The variable count is not accessible from the outside at all. Only the three methods returned by makeCounter can touch it. This is encapsulation without a class in sight.

This pattern of returning multiple functions that share private state shows up constantly in JavaScript. Once you see it, you will start noticing it in middleware, event handlers, and module patterns.

Function Composition with pipe and compose

So far you have seen functions that take simple values and return simple values. Higher-order functions also let you combine functions together to build larger operations from smaller pieces.

The idea is straightforward: instead of nesting calls like this:

const result = round(toFixed(add(2, 3), 2));

Nesting calls like this works for two or three functions, but it gets hard to read as the chain grows. The order of execution runs right to left, which is the opposite of how most people scan code.

You define a pipeline where data flows through each step in order.

function pipe(...fns) {
  return function piped(value) {
    return fns.reduce((v, fn) => fn(v), value);
  };
}

const add      = (a, b) => a + b;
const double   = x => x * 2;
const round    = x => Math.round(x);

const processNumber = pipe(add, double, round);

processNumber(2, 0); // 4

pipe(add, double, round) returns a new function. When you call that function with a value, it threads it through each function in sequence: add combines the value with a second argument, double scales the result, and round cleans it up.

compose does the same thing but in reverse order, applying the rightmost function first. pipe reads more naturally for step-by-step data transformations because the order you write the functions matches the order they execute.

This is declarative programming. You say what should happen (add, then double, then round) rather than imperatively managing the intermediate values yourself.

Chaining array operations

The real payoff comes when you chain these methods together. Each method in the chain receives the output of the previous one, so you can build complex transformations without temporary variables.

const orders = [
  { id: 1, total: 100, status: 'shipped' },
  { id: 2, total: 250, status: 'pending' },
  { id: 3, total: 75,  status: 'shipped' },
];

const totalRevenue = orders
  .filter(o => o.status === 'shipped')
  .map(o => o.total)
  .reduce((sum, t) => sum + t, 0);
// 175

Each line has a single, focused job. The sequence reads almost like English: keep shipped orders, grab their totals, add them up.

This chain pattern is clean because each step produces exactly what the next step needs. No shared mutable state, no intermediate variables cluttering the scope.

Higher-order functions are about shape

The useful part of a higher-order function is not that it is fancy. It is that the function’s shape lets you separate policy from data. The callback can carry the part that changes, while the higher-order wrapper keeps the surrounding workflow stable. That makes the code easier to test because the moving part is isolated, and it also makes the wrapper reusable because it does not care which specific callback you pass in.

Reach for small transforms first

Higher-order functions are easiest to understand when they are tiny and focused. A map, filter, or reduce callback should do one job clearly enough that the next person can read it without tracing a lot of branches. If the callback starts to grow, split the logic into named helpers. That keeps the higher-order pattern readable and prevents it from becoming a clever wrapper around complicated imperative code.

Keep the wrapper simple

A higher-order function should make the surrounding workflow easier to use, not harder to debug. If the wrapper begins hiding too much control flow, the caller loses the benefit of the abstraction. Small helpers that accept a callback, add a little structure, and return a clear result are usually the safest place to start. That approach keeps the function useful without turning the code into a puzzle.

Next steps

Take a piece of code you already have and rewrite one for loop as a chain of map, filter, and reduce. The exercise forces you to separate the what from the how — and once you do, you will notice how much of the original loop was scaffolding rather than actual logic.

Pair this with pure functions to get the full benefit. Pure callbacks combined with higher-order wrappers are easy to test in isolation, and they compose cleanly without shared mutable state getting in the way.

See Also