JavaScript Higher-Order Functions Explained: From map, filter, and reduce to Building once and memoize

Functions that take functions or return functions. How to use map, filter, and reduce well, how to build practical higher-order functions like once, memoize, pipe, and curried helpers yourself, and traps like ['1','2','3'].map(parseInt).

What does ['1', '2', '3'].map(parseInt) return?

Quick quiz. You want to turn an array of strings into numbers, so you write:

["1", "2", "3"].map(parseInt);

You probably expect [1, 2, 3]. The actual result is:

[1, NaN, NaN]

We'll solve that later in the post. First, let's look at what this code is built on: higher-order functions. map is one, and so is addEventListener, which you use every day.


What is a higher-order function?

A higher-order function is a function that takes a function as an argument, returns a function, or both.

This works in JavaScript because functions are first-class objects: you can treat a function like any other value, such as a number or a string.

First-class means you canJavaScript example
Store it in a variableconst greet = () => "hi";
Pass it to another functionbutton.addEventListener("click", greet);
Return it from another functionconst make = () => () => "hi";

Higher-order functions come in two broad kinds:

KindExamples
Functions that take functionsmap, filter, reduce, setTimeout, addEventListener
Functions that return functionsdebounce, once, memoize, bind

1. Functions that take functions: array methods

The higher-order functions you use most are array methods. Let's practice on this product data.

const products = [
  { name: "Keyboard", price: 89, category: "Electronics", inStock: true },
  { name: "Mouse", price: 39, category: "Electronics", inStock: false },
  { name: "Mug", price: 12, category: "Home", inStock: true },
  { name: "Monitor", price: 259, category: "Electronics", inStock: true },
  { name: "Tumbler", price: 25, category: "Home", inStock: true },
];

Let's get "the names of in-stock products" and "the total price of in-stock products."

// With a for loop
const names = [];
let total = 0;
for (let i = 0; i < products.length; i++) {
  if (products[i].inStock) {
    names.push(products[i].name);
    total += products[i].price;
  }
}

// With higher-order functions
const inStock = products.filter((p) => p.inStock);
const names = inStock.map((p) => p.name); // ['Keyboard', 'Mug', 'Monitor', 'Tumbler']
const total = inStock.reduce((sum, p) => sum + p.price, 0); // 385

A for loop makes you spell out how to iterate: the index, the condition, the push. Higher-order functions let you write only what to do: filter, transform, combine. The code says what it means.

Move the checkbox and slider in the preview below. Every change re-runs filter → map → reduce.

Common array higher-order functions at a glance

MethodWhat it doesReturnsExample result
mapTransforms each elementA new array of the same lengthArray of names
filterKeeps matching elementsA new array (length ≤ original)In-stock products
reduceCombines into one valueAny value (number, object, ...)Total 385
findFirst matching elementThe element or undefined"Monitor" (first over $100)
someDoes any element match?true / falseAnything out of stock? true
everyDo all elements match?true / falseAll over $10? true
forEachRuns something for each elementundefined(no return value)

Grouping with reduce

reduce is the most flexible of the bunch: it takes an array and can build a value of any shape. Let's group products by category.

const byCategory = products.reduce((acc, p) => {
  (acc[p.category] ??= []).push(p.name);
  return acc;
}, {});
// { Electronics: ['Keyboard', 'Mouse', 'Monitor'], Home: ['Mug', 'Tumbler'] }

Grouping is common enough that it became a standard function. Object.groupBy(products, (p) => p.category) does the same thing in one line (supported in major browsers since 2024 and in Node.js 21+).


2. Functions that return functions: build your own

Higher-order functions that return functions go hand in hand with closures: the returned function keeps remembering the variables from when it was created.

Function factories: functions that remember settings

const multiplier = (n) => (x) => x * n;

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

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

multiplier(2) builds and returns "a new function that remembers 2." Stamping out functions with settings baked in is the main use of higher-order functions that return functions.

once: a function that runs only once

Useful for preventing double-clicked payment buttons or initialization code running twice.

function once(fn) {
  let called = false;
  let result;
  return (...args) => {
    if (!called) {
      called = true;
      result = fn(...args);
    }
    return result;
  };
}

const init = once(() => {
  console.log("Initializing!");
  return 42;
});

init(); // logs "Initializing!", returns 42
init(); // logs nothing, returns 42

memoize: a function that remembers its results

Caches heavy calculations that get called with the same input again and again.

function memoize(fn) {
  const cache = new Map();
  return (arg) => {
    if (cache.has(arg)) return cache.get(arg);
    const result = fn(arg);
    cache.set(arg, result);
    return result;
  };
}

const slowSquare = (n) => {
  console.log(`Computing: ${n}`);
  return n * n;
};
const fastSquare = memoize(slowSquare);

fastSquare(4); // logs "Computing: 4", returns 16
fastSquare(4); // logs nothing, returns 16 straight from the cache

Adding behavior to an existing function

Without touching the original function, you can wrap it to add things like logging or permission checks before and after it runs.

function withLogging(fn) {
  return (...args) => {
    console.log(`Call: ${fn.name}(${args.join(", ")})`);
    const result = fn(...args);
    console.log(`Result: ${result}`);
    return result;
  };
}

function sum(a, b) {
  return a + b;
}

const loggedSum = withLogging(sum);
loggedSum(2, 3);
// Call: sum(2, 3)
// Result: 5

debounce and throttle, which you use to tame search-box input, follow exactly this shape: they take a function and return a new function that controls when it actually runs.


3. Function composition: chaining small functions

Connecting several small functions like a pipeline to make one function is called function composition. You can build it with reduce.

const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);

const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const slug = (s) => s.replace(/\s+/g, "-");

const toSlug = pipe(trim, lower, slug);
toSlug("  Hello World Nuxt  "); // "hello-world-nuxt"

Each function does one thing, and pipe runs them left to right. Reordering steps or adding new ones is easy.


4. Currying: taking arguments one at a time

Currying turns a function that takes several arguments into a chain of functions that each take one.

const add = (a) => (b) => a + b;

add(1)(2); // 3

const add10 = add(10);
[1, 2, 3].map(add10); // [11, 12, 13]

You can make a function with some arguments pre-filled (add10), which is handy for passing straight into higher-order functions like map.


Traps to watch for

Trap 1. map(parseInt): the answer to the quiz

map passes three arguments to its callback: (element, index, array). But parseInt treats its second argument as the radix.

CallWhat it meansResult
parseInt("1", 0)Radix 0 → treated as the default (base 10)1
parseInt("2", 1)Base 1 doesn't existNaN
parseInt("3", 2)There's no digit 3 in binaryNaN

The fix is to decide yourself which arguments get passed:

["1", "2", "3"].map(Number); // [1, 2, 3]
["1", "2", "3"].map((s) => parseInt(s, 10)); // [1, 2, 3]

When you pass a function directly to a higher-order function (map(parseInt)), check how it interprets extra arguments. When in doubt, wrapping it in an arrow function is the safe choice.

Trap 2. forEach returns nothing

const result = [1, 2, 3].forEach((x) => x * 2);
console.log(result); // undefined

If you need a new array, use map. If you just want to run something for each element, use forEach.

Trap 3. sort mutates the original

map and filter return new arrays, but sort, reverse, and splice change the original array itself. That's especially dangerous with React or Vue state. To keep the original intact, use the methods added in ES2023.

const nums = [3, 1, 2];
const sorted = nums.toSorted((a, b) => a - b);

nums; // [3, 1, 2] (unchanged)
sorted; // [1, 2, 3]
Mutates the originalReturns a new array
sort()toSorted()
reverse()toReversed()
splice()toSpliced()
arr[i] = xwith(i, x)

Trap 4. Side effects inside map

A map callback is for "transforming a value and returning it." If you call an API or change outside variables inside it, a reader looking at map can't tell that's happening. Keep the roles separate: transform with map, run things with forEach or for...of.


Summary: why this is worth knowing

What you wantHigher-order function
Transform / filter / combine an arraymap / filter / reduce
Group itemsreduce or Object.groupBy
Stamp out functions with settings baked inA function that returns a function (multiplier)
Run only onceonce
Cache resultsmemoize
Add behavior to an existing functionA wrapper (withLogging, debounce)
Chain small functionspipe

The heart of higher-order functions is separating "what to do" from "how to iterate." Hand the "how" (looping, caching, limiting how often something runs) to a higher-order function, and you only write the small functions that describe the "what." Those small functions are easy to test and easy to reuse. Next time you're about to write a for loop with a temporary array, first check whether map, filter, or reduce can say what you mean in one line.