Higher-Order Functions and Callbacks
Functions that accept or return other functions - the pattern behind map, filter, and every event listener you've ever written.
A higher-order function is a function that takes another function as an argument, returns one, or both. JavaScript treats functions as values that can be stored, passed, and returned like numbers or strings. That's the whole feature.
Passing a callback to a higher-order function is the most common pattern. Array methods like map, filter, and forEach are built-in higher-order functions. The callback you pass is just a value.
const numbers = [1, 2, 3, 4, 5];
// Passing an arrow function as a callback
const doubled = numbers.map((n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// The callback is a value - you can store it first
const isEven = (n) => n % 2 === 0;
const evens = numbers.filter(isEven);
console.log(evens); // [2, 4]
// setTimeout is a higher-order function too
setTimeout(() => console.log("done"), 0);A function can return another function. This pattern is called a factory or a closure. Each call to the outer function produces an independent inner function with its own captured variables.
function multiplier(factor) {
return (x) => x * factor;
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(double(10)); // 20
// Each function independently remembers its factor
const taxes = [100, 250, 80].map(multiplier(1.2));
console.log(taxes); // [120, 300, 96]Array methods pass three arguments to every callback: the current element, its index, and the full array. Passing a function directly as a callback works fine when it only uses the first argument. Problems appear when it reads more.
// This looks fine but breaks
const result = ["1", "2", "3"].map(parseInt);
console.log(result); // [1, NaN, NaN]
// Why: map calls parseInt(element, index, array)
// parseInt("1", 0) = 1 (radix 0 treated as 10)
// parseInt("2", 1) = NaN (radix 1 is invalid)
// parseInt("3", 2) = NaN (radix 2, "3" is not binary)
// Fix: wrap so only the element is forwarded
const correct = ["1", "2", "3"].map((s) => parseInt(s, 10));
console.log(correct); // [1, 2, 3]
// Same issue with Number - but Number ignores extra args, so it's safe
const nums = ["1", "2", "3"].map(Number);
console.log(nums); // [1, 2, 3]In production
Callback signatures are a contract. When an array method passes (value, index, array) and your callback reads more arguments than intended, you get silent misbehavior rather than an error. The parseInt example above is the classic case, but the same bug can appear with any multi-parameter function. Wrapping in an arrow ((s) => fn(s)) makes the intent explicit and breaks the coupling. TypeScript makes this class of bug impossible: it will flag a signature mismatch at compile time.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free