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 can | JavaScript example |
|---|---|
| Store it in a variable | const greet = () => "hi"; |
| Pass it to another function | button.addEventListener("click", greet); |
| Return it from another function | const make = () => () => "hi"; |
Higher-order functions come in two broad kinds:
| Kind | Examples |
|---|---|
| Functions that take functions | map, filter, reduce, setTimeout, addEventListener |
| Functions that return functions | debounce, 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
| Method | What it does | Returns | Example result |
|---|---|---|---|
| map | Transforms each element | A new array of the same length | Array of names |
| filter | Keeps matching elements | A new array (length ≤ original) | In-stock products |
| reduce | Combines into one value | Any value (number, object, ...) | Total 385 |
| find | First matching element | The element or undefined | "Monitor" (first over $100) |
| some | Does any element match? | true / false | Anything out of stock? true |
| every | Do all elements match? | true / false | All over $10? true |
| forEach | Runs something for each element | undefined | (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.
| Call | What it means | Result |
|---|---|---|
| parseInt("1", 0) | Radix 0 → treated as the default (base 10) | 1 |
| parseInt("2", 1) | Base 1 doesn't exist | NaN |
| parseInt("3", 2) | There's no digit 3 in binary | NaN |
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 original | Returns a new array |
|---|---|
| sort() | toSorted() |
| reverse() | toReversed() |
| splice() | toSpliced() |
| arr[i] = x | with(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 want | Higher-order function |
|---|---|
| Transform / filter / combine an array | map / filter / reduce |
| Group items | reduce or Object.groupBy |
| Stamp out functions with settings baked in | A function that returns a function (multiplier) |
| Run only once | once |
| Cache results | memoize |
| Add behavior to an existing function | A wrapper (withLogging, debounce) |
| Chain small functions | pipe |
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.