What are higher-order functions in JavaScript (hof)
Higher-order function (HOF) - a function that takes another function as an argument, returns a function as a result, or does both.
Theory
TL;DR
- Functions in JavaScript are values - pass them around like numbers or strings
- HOF = takes a function as input, or returns one, or both
- Built-in examples you already know:
map,filter,reduce,forEach - Use HOF when the same operation repeats with different behavior - pass the behavior in instead of copying code
- Decision rule: if you catch yourself writing the same loop with a slightly different operation inside, a HOF probably belongs there
Quick example
// map() is a HOF - accepts a function, applies it to each element
const numbers = [1, 2, 3];
const doubled = numbers.map(num => num * 2); // [2, 4, 6]
// multiplier() is a HOF - returns a new configured function
function multiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplier(2);
console.log(double(5)); // 10map takes num => num * 2 as an argument. multiplier builds and returns a new function configured with factor. Both are HOFs.
Why HOFs exist
Without HOFs, you repeat logic. With them, you write it once and pass in what changes. That is the whole point.
Consider transforming an array: without map, you write a for loop every time, declare a new array, manually push results. With map, you say "here is what to do per item" and it handles the rest. The operation stays consistent; the behavior you inject varies.
I have seen developers copy-paste the same for loop five times with slightly different transformations inside. A map call would have been one line each time.
This works because JavaScript treats functions as first-class citizens. They are objects stored on the heap, assignable to variables, passable as arguments, returnable from other functions. A HOF just uses that deliberately.
When to use
- Transforming collections:
map,filter,reducefor any list of API data or UI items - Event handling:
addEventListenertakes a callback - your function runs when the event fires - Async flows:
Promise.then()takes a function to run on resolve - Express middleware: each middleware is a HOF that wraps the next handler with extra logic
- Specialized functions:
multiplier(2)gives youdouble,multiplier(3)gives youtriple, from one definition
For simple one-off logic where a plain loop reads more clearly, skip the HOF.
How it works internally
V8 treats functions as objects. When you pass a function to a HOF, V8 copies a reference to that object, not the code itself. When a HOF returns a function, the returned function captures the outer scope through a closure - V8 allocates a closure cell on the heap holding references to lexical variables, keeping them alive as long as the inner function lives. Built-in methods like Array.prototype.map are implemented in C++ and call your JS callback on each iteration.
Common mistakes
Forgetting to call the returned function:
const doubler = multiplier(2); // Returns a function
console.log(multiplier(2)); // Logs [Function] - not a number!
console.log(doubler(5)); // 10 - this is what you wantedmultiplier(2) hands you a function. You still need to call it with the actual value.
Mutating the passed function:
// Bad: adds a property to the original function object
function badHOF(fn) {
fn.customProp = 'mutated'; // Changes caller scope
return fn;
}
// Fine: wrap it, leave the original alone
function goodHOF(fn) {
return (...args) => fn(...args);
}Functions are objects. Adding properties to them affects the original outside your HOF.
Losing this context in callbacks:
const obj = { value: 42 };
[1, 2].forEach(function() {
console.log(this.value); // undefined - context is lost
});
[1, 2].forEach(function() {
console.log(this.value); // 42 - explicit binding restores it
}.bind(obj));Unstable HOF references in React deps:
// Bad: new function reference every render triggers effect each time
useEffect(() => { fetchData(increment); }, [increment]);
// Good: useCallback stabilizes the reference
const increment = useCallback(() => setCount(c => c + 1), []);React compares dependencies by reference. A new function each render looks like a changed dependency and re-runs the effect.
Real-world usage
Array.map/filter/reduce: every React list render, Lodash data pipelines- Express middleware:
app.use(authMiddleware(handler))- each middleware wraps the next handler Promise.then(): chains async work by passing handlers- React
useCallback: memoizes a function to keep its reference stable between renders - Lodash
_.curry/_.partial: build specialized functions from general ones
Follow-up questions
Q: What is the difference between a HOF and a callback?
A: A callback is any function passed as an argument. A HOF is the function that receives it. map is the HOF; num => num * 2 is the callback.
Q: Implement map from scratch.
A:
function myMap(arr, fn) {
const result = [];
for (let i = 0; i < arr.length; i++) {
result.push(fn(arr[i], i, arr));
}
return result;
}Q: How do closures relate to HOFs?
A: Most HOFs that return functions rely on closures. The returned function keeps access to variables from the outer scope - like factor inside multiplier - even after the outer call has finished.
Q: Do HOFs have a performance cost?
A: In tight loops, yes. Returned functions create closure allocations on the heap. For 1M+ items, a manual loop can run 2-3x faster in V8. In most application code that threshold does not matter.
Q: In React 18 concurrent mode, why does an un-memoized HOF in useEffect deps cause an infinite loop?
A: Each render creates a new function reference. React compares dependencies by reference, so a new function looks like a changed dep. The effect re-runs, triggers another render, which creates another new function. useCallback with a correct deps array breaks the cycle.
Examples
Basic: wrapping any function with a logger
function withLogging(fn) {
return (...args) => {
console.log('Calling:', fn.name, 'with args:', args);
const result = fn(...args);
console.log('Returned:', result);
return result;
};
}
function add(a, b) { return a + b; }
const loggedAdd = withLogging(add);
loggedAdd(3, 4);
// Calling: add with args: [3, 4]
// Returned: 7withLogging returns a new function that wraps whatever you pass in. The original add is untouched. This is the decorator pattern: extend behavior without changing the source function.
Intermediate: Express route validation middleware
const express = require('express');
const app = express();
app.use(express.json());
// HOF: accepts a route handler, returns a new handler with validation built in
function validateUser(fn) {
return (req, res, next) => {
if (!req.body.username) {
return res.status(400).send('Missing username');
}
return fn(req, res, next);
};
}
const userHandler = (req, res) => res.json({ user: req.body.username });
app.post('/user', validateUser(userHandler));
// POST /user { username: 'alice' } => 200 { user: 'alice' }
// POST /user {} => 400 "Missing username"validateUser wraps any route handler with a validation check. The handler itself knows nothing about validation. Swap in requireAdmin or checkRateLimit and the pattern is identical.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.