Higher-Order Functions
A higher-order function (HOF) is any function that does at least one of two things: it accepts another function as an argument, or it returns a function as its result. Because functions in JavaScript are first-class values — you can store them in variables, pass them around, and return them — higher-order functions fall out naturally from the language. They are the foundation of functional programming in JavaScript and power everyday tools like map, filter, reduce, event handlers, and middleware.
What makes a function “higher-order”
A first-order function only deals with plain data — numbers, strings, objects. A higher-order function elevates functions themselves to data. This abstraction lets you write reusable behavior once and customize it by injecting logic from the caller.
// Takes a function as an argument
function repeat(times, action) {
for (let i = 0; i < times; i++) {
action(i);
}
}
repeat(3, (i) => console.log(`Tick ${i}`));
Output:
Tick 0
Tick 1
Tick 2
repeat doesn’t know or care what the action does — it only knows how to call it. That separation of “how many times” from “what to do” is the essence of higher-order design.
Returning a function: factories and closures
A function that returns another function lets you preconfigure behavior. The returned function “remembers” the variables from the scope where it was created — a closure. The classic example is an adder factory.
function makeAdder(base) {
return (n) => base + n;
}
const add5 = makeAdder(5);
const add100 = makeAdder(100);
console.log(add5(2)); // 7
console.log(add100(2)); // 102
Output:
7
102
Here makeAdder is the higher-order function, and base stays alive inside each returned arrow function. This pattern is everywhere: building configured loggers, rate limiters, validators, and theme-aware helpers.
function withPrefix(prefix) {
return (message) => `[${prefix}] ${message}`;
}
const logError = withPrefix("ERROR");
const logInfo = withPrefix("INFO");
console.log(logError("Disk full"));
console.log(logInfo("Backup complete"));
Output:
[ERROR] Disk full
[INFO] Backup complete
Passing functions: map, filter, and reduce
The built-in array iteration methods are the most common higher-order functions you’ll use. Each accepts a callback and applies it across the collection.
| Method | Callback receives | Returns | Use it to |
|---|---|---|---|
map | (item, index, array) | a new array | transform every element |
filter | (item, index, array) | a new array | keep elements that pass a test |
reduce | (acc, item, index, array) | a single value | collapse a list into one result |
forEach | (item, index, array) | undefined | run a side effect per element |
const orders = [
{ id: 1, total: 30, paid: true },
{ id: 2, total: 50, paid: false },
{ id: 3, total: 20, paid: true },
];
const paidTotal = orders
.filter((o) => o.paid)
.map((o) => o.total)
.reduce((sum, t) => sum + t, 0);
console.log(paidTotal);
Output:
50
Each step is a higher-order call, and chaining them reads almost like a sentence: keep the paid orders, take their totals, sum them up.
Prefer
map/filter/reduceover manualforloops when you’re producing a new value from a collection — they signal intent and avoid off-by-one bugs. Reach for a plain loop only when you need to break early or mutate in place for performance.
Composing functions
Because HOFs both take and return functions, you can build small, single-purpose functions and glue them together. Composition runs the output of one function into the next.
const compose =
(...fns) =>
(input) =>
fns.reduceRight((value, fn) => fn(value), input);
const trim = (s) => s.trim();
const toLower = (s) => s.toLowerCase();
const slugify = (s) => s.replace(/\s+/g, "-");
const makeSlug = compose(slugify, toLower, trim);
console.log(makeSlug(" Hello World "));
Output:
hello-world
compose is itself a higher-order function: it takes several functions and returns a brand-new one. Read right-to-left, makeSlug trims, lowercases, then slugifies. If you prefer left-to-right reading order, swap reduceRight for reduce and call it pipe.
A practical example: a debounce wrapper
Returning functions shines for cross-cutting concerns. debounce wraps any function so it only fires after activity settles — perfect for search inputs and resize handlers.
function debounce(fn, delay = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
const search = debounce((query) => {
console.log(`Searching for: ${query}`);
}, 200);
search("ja");
search("jav");
search("java"); // only this call runs, after 200ms
Output:
Searching for: java
debounce accepts a function and returns an enhanced version of it — a higher-order function on both counts. The original fn is preserved by closure and invoked only when the timer expires.
Best practices
- Keep callbacks small and pure where possible; a callback with no side effects is easier to test and reuse.
- Name returned functions or assign them to descriptive variables so stack traces and intent stay clear.
- Favor
map/filter/reducefor transformations, and reserveforEachfor genuine side effects. - Use the spread
(...args)pattern in wrapper functions so they forward every argument transparently. - Avoid over-composing — three or four chained transformations are readable; ten are not.
- Remember each returned function captures its enclosing scope; watch for unintentionally retained large objects to prevent memory leaks.