Free lesson · JavaScript Basic to Advance
Higher-Order Functions
Functions That Work With Other Functions
A higher-order function is a function that does one of two things: it takes another function as a parameter, or it returns a function. Either of those qualifies. Many JavaScript functions do both.
// Takes a function as a parameter
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(function(n) {
return n * 2;
});
console.log(doubled);
// Returns a function
function makeMultiplier(factor) {
return function(n) {
return n * factor;
};
}
const triple = makeMultiplier(3);
console.log(triple(5));
console.log(triple(10));
Both map and makeMultiplier are higher-order functions. map takes a function and applies it to each element. makeMultiplier returns a customized function based on the factor you give it.
I'm Nikhil. Topics 5.6 and 5.7 are mine because this is where the JavaScript conceptual shift happens — from "functions are blocks of code I run" to "functions are values I move around." Once you internalize this shift, everything in the rest of the course gets easier. Closures (Module 6), promises (Module 9), the entire functional style of modern JavaScript — all of it depends on understanding that functions can be passed, stored, and returned just like any other value.
This Topic and the next are the most conceptually important Topics in Module 5. Spend time here.
Why Higher-Order Functions Exist
Higher-order functions let you write code that's flexible about what it does, not just with what data.
Without higher-order functions, every operation you wanted to do on an array would need its own function:
// Without higher-order functions, you'd need a separate function per operation
function doubleAll(numbers) {
const result = [];
for (const n of numbers) {
result.push(n * 2);
}
return result;
}
function squareAll(numbers) {
const result = [];
for (const n of numbers) {
result.push(n * n);
}
return result;
}
console.log(doubleAll([1, 2, 3]));
console.log(squareAll([1, 2, 3]));
Notice the pattern: both functions are identical except for the operation inside the loop. The repetition is wasteful — every new operation needs its own duplicated function.
With map, the operation becomes a parameter:
function transform(numbers, operation) {
const result = [];
for (const n of numbers) {
result.push(operation(n));
}
return result;
}
console.log(transform([1, 2, 3], n => n * 2)); // [2, 4, 6]
console.log(transform([1, 2, 3], n => n * n)); // [1, 4, 9]
console.log(transform([1, 2, 3], n => n + 100)); // [101, 102, 103]
One transform function. Infinitely many operations. That's what higher-order functions buy you: code that varies in behavior without varying in structure.
Array.prototype.map is the built-in version of this pattern. Same idea, used constantly in real JavaScript code.
Interactive animation: The built-in versions of this pattern - map transforms, filter selects and reduce accumulates, each driven by a callback.
Callbacks
A callback is a function passed to another function with the expectation that it will be called at some point. The receiving function controls when (and whether) the callback runs.
function processWithLogging(value, callback) {
console.log(`Processing ${value}...`);
const result = callback(value);
console.log(`Done. Result: ${result}`);
return result;
}
processWithLogging(5, n => n * 2);
processWithLogging("hello", s => s.toUpperCase());
processWithLogging is a higher-order function (takes a function as parameter). The function it takes is a callback — it gets called inside processWithLogging's body.
Callbacks are the foundation of how JavaScript handles many things:
- Iteration:
array.forEach(callback),array.map(callback),array.filter(callback) - Event handling:
button.addEventListener("click", callback) - Asynchronous operations:
setTimeout(callback, 1000),fetch(url).then(callback) - Sorting:
array.sort(compareCallback)
In each case, the receiving function controls when the callback gets called. You're delegating "what to do" while the receiving function handles "when to do it."
graph LR
A[Your code] -->|passes callback| B[Higher-Order Function]
B -->|calls callback| C[Your callback runs]
C -->|returns value| B
B -->|returns result| A
style B fill:#7c3aed,color:#fff
This diagram shows the relationship between your code and a higher-order function with a callback. Your code passes a callback function as an argument to the higher-order function. The higher-order function decides when and how to call your callback. When the callback runs, it returns a value back to the higher-order function. The higher-order function then returns a final result back to your code. The key insight is that you're delegating control of when the callback runs while keeping control of what it does.
Built-in Higher-Order Methods on Arrays
JavaScript arrays come with several higher-order methods. These are the ones you'll use constantly:
forEach — visit each element (no return value):
const founders = ["Olena", "Nikhil", "Emeka"];
founders.forEach(name => console.log(`Founder: ${name}`));
map — transform each element into a new array:
const numbers = [1, 2, 3, 4];
const squared = numbers.map(n => n * n);
console.log(squared);
filter — keep elements that match a predicate:
const numbers = [1, 2, 3, 4, 5, 6];
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens);
reduce — combine all elements into a single value:
const numbers = [1, 2, 3, 4, 5];
const total = numbers.reduce((sum, n) => sum + n, 0);
console.log(total);
find — return the first element matching a predicate (or undefined):
const users = [{age: 25}, {age: 30}, {age: 18}];
const minor = users.find(u => u.age < 21);
console.log(minor);
some and every — test whether some/all elements match a predicate:
const ages = [25, 30, 18, 40];
console.log(ages.some(a => a < 21)); // true — at least one
console.log(ages.every(a => a < 21)); // false — not all
sort — sort in place, using a comparison callback:
const numbers = [3, 1, 4, 1, 5, 9, 2, 6];
numbers.sort((a, b) => a - b);
console.log(numbers);
Each of these is a higher-order function: it takes a function (the callback) and uses it to determine how to operate on the array.
Interactive animation: reduce traced step by step - the sum starts at the seed 0 and each callback return becomes the next sum.
Functions That Return Functions
The other half of higher-order functions: functions that return functions. This is one of the most powerful patterns in JavaScript and the gateway to closures (Module 6).
function makeAdder(x) {
return function(y) {
return x + y;
};
}
const addFive = makeAdder(5);
const addTen = makeAdder(10);
console.log(addFive(3)); // 8
console.log(addFive(100)); // 105
console.log(addTen(3)); // 13
makeAdder returns a customized function. Each call to makeAdder produces a new function — one that "remembers" the value of x it was created with.
This is the closure mechanism (covered in Module 6). The returned function has access to x from the enclosing scope, even after makeAdder has finished running. That memory of an enclosing scope is what defines a closure.
Real-world uses of "functions that return functions":
Configuration helpers — create a customized utility once, use it many times:
function makeValidator(minLength, maxLength) {
return function(value) {
return value.length >= minLength && value.length <= maxLength;
};
}
const isShortName = makeValidator(2, 20);
const isLongDescription = makeValidator(50, 500);
console.log(isShortName("Olena")); // true
console.log(isShortName("O")); // false
console.log(isLongDescription("Hello")); // false
Partial application — fix some arguments now, fill in the rest later:
function multiply(a, b) {
return a * b;
}
function partial(fn, fixedArg) {
return function(remainingArg) {
return fn(fixedArg, remainingArg);
};
}
const triple = partial(multiply, 3);
const double = partial(multiply, 2);
console.log(triple(7)); // 21
console.log(double(7)); // 14
Both triple and double are derived from multiply. We've created specialized functions from a general one by fixing one of its arguments. This is a core technique in functional programming.
Interactive animation: The factory pattern - each call returns a new function that remembers the value it was configured with.
Common Pitfalls
Warning: The higher-order function traps.
Calling a function when you meant to pass it. Writing
setTimeout(myFunction(), 1000)callsmyFunctionimmediately and passes whatever it returned. You meantsetTimeout(myFunction, 1000)to pass the function itself sosetTimeoutcan call it later. This is one of the most common bugs with callbacks.Forgetting that callbacks aren't called immediately. When you write
button.addEventListener("click", handleClick),handleClickis not called at that line. It's stored, then called later when a click happens. New JavaScript developers often try to debug callbacks by addingconsole.lognext to where they pass them — but that runs before the callback runs.Returning the wrong thing from a callback. Many higher-order methods depend on what your callback returns.
filterexpects a boolean.mapuses the return value as the transformed element.sortexpects a number. Returning the wrong type produces silently incorrect results.
Why This Matters in Interviews
Interview: Higher-order functions are central to modern JavaScript and central to JavaScript interviews. Many problems are most cleanly solved using
map,filter,reduce. Interviewers watch whether you reach for these naturally or whether you write everything as classic for loops."Implement your own
map" is a classic intermediate interview question — it tests whether you understand howmapworks internally and whether you can reason about callbacks.Topic 5.8 — Interview Lens: Functions and Higher-Order Patterns — covers the deeper interview treatment of these patterns, including the "implement this method" challenges.
Try It
Exercise: Implement your own version of
mapfrom scratch, calledmyMap, that takes an array and a callback function and returns a new array with the callback applied to each element. Don't use the built-inmap. Use afororfor...ofloop.function myMap(array, callback) { // your implementation here } console.log(myMap([1, 2, 3], n => n * 2)); // should print [2, 4, 6] console.log(myMap(["a", "b"], s => s.toUpperCase())); // should print ["A", "B"]Once you've got that working, try implementing
myFilter(returns elements where callback returns true) andmyReduce(combines all elements using an accumulator). Building these from scratch is the best way to internalize how higher-order functions actually work.