Higher-order functions
Use functions as values: pass callbacks into helpers, return customized functions from factories, compare declarative array pipelines with loops, and revisit map, filter, and reduce with professional edge cases.
- 01Recognize higher-order functionsTell whether code takes a function, returns a function, or simply calls one.
- 02Swap behavior safelyPass callbacks without accidentally calling them too early, and read callback signatures.
- 03Build clear data pipelinesChoose loops or declarative methods based on the shape and readability of the task.
The big idea
A higher-order function is a function that works with other functions. More precisely, it either takes a function as an argument, returns a function, or both. This is not a special syntax feature. It works because JavaScript treats functions as values: you can store them in variables, put them in objects, pass them around, and call them later.
You have already met the idea in the Callbacks, Arrow functions, Closures, and map, filter & reduce lessons. This lesson ties those pieces together at the start of the Functional programming module.
A higher-order function lets you separate the repeated shape of work from the behavior plugged into that work.
A stand mixer does not need a different motor for cake batter, bread dough, and whipped cream. You keep the motor and swap the attachment. Higher-order functions give code the same trick: keep the control flow and swap the function that customizes it.
- In real life: The stand mixer body
- In JavaScript: The higher-order function, such as
applyToEachormap - In real life: A whisk, dough hook, or paddle
- In JavaScript: The callback function you plug in
- In real life: The bowl turning the same way
- In JavaScript: The repeated loop or orchestration
- In real life: Different food from the same machine
- In JavaScript: Different results from the same helper
Where the analogy stops: A mixer can only use attachments made for it. JavaScript will accept any value, so a higher-order helper must be called with a real function or it throws when it tries to call it.
We will move from intuition to precision: first functions as values, then callbacks, then factories that return functions, then the practical difference between imperative loops and declarative pipelines.
Functions as values
A function declaration or function expression creates a value. The value is callable, but it is still a value. That means these two lines do very different jobs:
const double = n => n * 2; applyToEach([1, 2, 3], double); // pass the function valueapplyToEach([1, 2, 3], double()); // call now, then pass the resultThe first call gives applyToEach the function so the helper can call it once per item. The second line tries to run double immediately, before any item exists. In real code that usually means a missing argument, a surprising result, or a runtime error.
| Expression | Meaning | Common use |
|---|---|---|
double | The function value itself | Pass as a callback |
double(3) | Call the function now with 3 | Compute a result immediately |
() => double(3) | A new function that will call later | Delay work until an event or callback |
This tiny distinction is the doorway to higher-order functions. Once you can pass behavior around, a helper can say, “I know how to repeat, wait, wrap, sort, or combine. You tell me what to do at the important moment.”
Functions as arguments
INTERACTIVEA function passed into another function is usually called a callback. The higher-order function decides when to call it and what arguments to provide. The callback decides the custom behavior.
Try one helper named applyToEach. It always loops through the same items, but you can swap in a callback that doubles numbers, labels them, or checks whether they are even.
function applyToEach(items, fn) { const results = []; for (const item of items) { results.push(fn(item)); } return results;} const numbers = [1, 2, 3];applyToEach(numbers, double);applyToEach(numbers, label);applyToEach(numbers, isEven);[1, 2, 3]double[2,4,6]The for...of loop stays exactly the same. The function you pass in decides what each item becomes.
double turns each number into twice itself. The loop does not change; only the callback behavior changes.
double(), you would call it immediately and pass the result, not the callback.This is the same principle behind array methods. map, filter, reduce, sort, and DOM event methods all accept callbacks. They differ in what they pass to the callback and what they do with the return value.
Array callbacks receive more than one argument: typically the element, the index, and the whole array. That is useful when you need it, and surprising when you pass a function that treats the second argument specially.
const words = ["1", "2", "3"];console.log(words.map(parseInt));console.log(words.map(word => parseInt(word, 10)));The first log prints [1, NaN, NaN]. Why? map calls the callback as callback(element, index, array), and parseInt reads its second argument as the number base. So it runs like parseInt("2", 1) and parseInt("3", 2), which are not useful parses. Wrap it: word => parseInt(word, 10).
Returning functions
STEP THROUGHHigher-order functions can also create and return functions. These are often called function factories. You call the factory once with settings, and it gives you back a function you can reuse many times.
A stamp maker turns one description into a reusable tool. You do not explain “Hi” every time you stamp a card. You make the stamp once, then press it whenever you need the same pattern.
- In real life: Describe the stamp once
- In JavaScript: Call
makeGreeter("Hi")ormakeMultiplier(3) - In real life: Receive a physical stamp
- In JavaScript: Store the returned function
- In real life: Press it many times
- In JavaScript: Call the returned function many times
- In real life: Each stamp keeps its design
- In JavaScript: Each closure keeps its remembered values
Where the analogy stops: A stamp's design is fixed. A returned JavaScript function can remember values that later change too, as makeCounter does.
Step through the factory below. Change the factor, then watch the returned function keep using that factor in later calls. This replay is recorded from the lesson’s real functions; it is a guided trace, not a browser engine debugger.
Choose a factory argument, predict both results, then step through the factory call and the later calls to the returned function.
script
function makeMultiplier(factor) { return function (value) { return value * factor; };} multiplier(4);multiplier(10);The closure is the reason the returned function can still read factor. The outer call has finished, but JavaScript keeps the lexical environment alive because the returned function needs it. The Closures and Closure patterns lessons go deeper into that memory model.
function makeMultiplier(n) { return value => value * n;} function makeCounter() { let count = 0; return () => { count += 1; return count; };} function once(fn) { let called = false; let result; return (...args) => { if (!called) { called = true; result = fn(...args); } return result; };} function withLogging(fn) { return value => { console.log("calling with", value); return fn(value); };}makeMultiplier(3)creates a reusable “times 3” function.makeCounter()creates private state without a class.once(fn)protects work that should happen one time.withLogging(fn)adds behavior around an existing function.
Each returned function keeps the values from the call that created it: the multiplier's n, the counter's count, and once's called flag.
Declarative vs imperative
COMPAREHigher-order functions often make code more declarative. Imperative code describes every mechanical step. Declarative code names the desired transformations and lets trusted helpers handle the repetition.
At a restaurant you usually describe the result you want. You do not explain dough hydration, oven temperature, and cheese placement unless you are training the chef. Declarative code is similar: it communicates intent first.
- In real life: Order “large pizza, no onions”
- In JavaScript: Say
orders.filter(...).reduce(...) - In real life: Give the cook every chopping and baking step
- In JavaScript: Write every loop, branch, push, and addition
- In real life: The kitchen still performs the steps
- In JavaScript: The array methods still loop internally
Where the analogy stops: Sometimes you really are the cook. If the steps are unusual, performance-sensitive, or need early exits, an explicit loop can be clearer.
Compare the same small report two ways: keep paid orders, format labels, and total their prices. The counted visits are deliberately visible so you do not confuse “more declarative” with “always fewer operations.”
const paid = orders.filter(order => order.paid);const paidLabels = paid.map(order => order.id + ": $" + order.total);const total = paid.reduce((sum, order) => sum + order.total, 0);const steps = orders.length + paid.length + paid.length; console.log(paidLabels);console.log(total);console.log(steps);["A100: $18","C300: $24"]427A simple loop can still be clearer for complex branching, early exits, or heavy performance-sensitive work.
The pipeline names the intent: keep paid orders, format labels, reduce to a total. It does more callback visits here, but the business rule is easy to scan.
Use the version your teammate can safely change next month. For a straight “keep, transform, combine” flow, a pipeline reads like the business rule. For nested branches, breaking early, or mutating a shared resource, a loop may say the truth more clearly.
map, filter & reduce revisited
EDGE CASESThe earlier array lesson introduced the job of each method. Now read them as higher-order functions. map takes a callback and returns a new array. filter takes a predicate callback and returns the items that pass. reduce takes a reducer callback and returns whatever the accumulator becomes.
const orders = [ { status: "paid", total: 18 }, { status: "draft", total: 9 }, { status: "paid", total: 24 }]; const grouped = orders.reduce((groups, order) => { groups[order.status] = (groups[order.status] ?? 0) + 1; return groups;}, {}); const mapped = [1, 2, 3].reduce((result, n) => { result.push(n * 2); return result;}, []); const filtered = [1, 2, 3, 4].reduce((result, n) => { if (n % 2 === 0) result.push(n); return result;}, []); console.log(grouped);console.log(mapped);console.log(filtered);console.log([].reduce((a, b) => a + b));The first reducer groups orders into an object by status. The next two show that reduce is powerful enough to implement map and filter: start with an empty array, push the right results, and return the accumulator. That is useful to understand, but usually not the clearest production code. Prefer the specific method when the specific method says the job.
Without an initial value, reduce uses the first array element as the accumulator. On an empty array there is no first element, so [].reduce((a, b) => a + b) throws a TypeError. Provide an initial value when the array might be empty.
Also remember callback signatures. Array callbacks receive element, index, and array. That is why arr.map(parseInt) surprises and why a sorter callback has its own shape: (a, b) => number. Not every callback API passes the same arguments.
Where you’ll use this
Higher-order functions are not just a functional-programming exercise. They show up in everyday frontend work:
button.addEventListener("click", handleSave); const visibleNames = users .filter(user => user.active) .map(user => user.name); app.use(withLogging(authMiddleware)); <ProductList products={products} onSelect={product => setSelected(product.id)}/>;- Event handlers store callbacks for later: the browser calls
handleSavewhen the click happens. - Array pipelines filter and map API data into display data.
- Middleware and wrappers add logging, authentication, timing, or retries around a function.
- React-style components often receive callbacks like
onSelectso a child can report a user action without knowing what the parent will do.
numbers.map(n => n * 2)makeMultiplier(3)withLogging(saveOrder)Number('42')button.addEventListener('click', handleClick)once(sendReceipt)items.sort((a, b) => a.score - b.score)names.join(', ')
Sort each expression by whether it takes a function, returns a function, does both, or does neither.
Common misconceptions
“Higher-order means complicated.”
The definition is small: takes a function or returns one. The complexity comes from unclear callback names, hidden side effects, or trying to be clever when a loop would be clearer.
“Callbacks and higher-order functions are the same thing.”
A callback is the function being passed. The higher-order function is the function receiving it, returning it, or wrapping it.
“You always write fewer operations with pipelines.”
A chain can visit arrays multiple times. That is often fine for readability, but a single loop can be more direct for hot paths.
“reduce is the advanced replacement for everything.”
reduce can build almost anything, but specific methods communicate intent better. Use map for one output per input and filter for keep-or-drop tests.
“A returned function shares settings with every factory call.”
Each factory call gets its own remembered values. Assigning the same returned function to another variable shares state; calling the factory again creates a fresh closure.
| Idea | Focus | Typical code | Watch out for |
|---|---|---|---|
| Callback | A function passed in to be called later or repeatedly | items.map(fn) | Pass fn, not fn() |
| Function factory | A function that creates a customized function | makeMultiplier(3) | Each call can remember separate values |
| Wrapper | A function around another function | withLogging(save) | Return the original result unless you mean to change it |
| Declarative pipeline | Name the transformations, not every loop step | orders.filter(...).map(...) | Too many tiny steps can hide simple logic |
Practice exercises
5 EXERCISESRead the helper and predict the exact array printed.
function applyToEach(items, fn) {
const results = [];
for (const item of items) results.push(fn(item));
return results;
}
console.log(applyToEach([2, 4], n => n + 1));applyToEach runs the callback for 2 and 4. The two return values are collected, so the console prints [3, 5].
Paste the program into your console. Then change the factory argument and call argument to make your own multiplier.
function makeMultiplier(n) {
return value => value * n;
}
const times4 = makeMultiplier(4);
console.log(times4(6));function makeMultiplier(n) {
return value => value * n;
}
const times4 = makeMultiplier(4);
console.log(times4(6));makeMultiplier(4) returns a function that remembers n as 4. Calling times4(6) returns 24.
Use the wrapper to reason about code that should run once, such as sending a receipt or initializing a widget.
function once(fn) {
let called = false;
let result;
return (...args) => {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
}
let sends = 0;
const sendOnce = once(() => {
sends += 1;
return sends;
});
console.log(sendOnce());
console.log(sendOnce());
console.log(sends);The first sendOnce() calls the original function, stores result 1, and flips called. The second returns the stored result. sends is still 1, so the logs are 1, 1, 1.
Explain why the pipeline reads as “keep paid orders, then total them.” Then check the printed number.
const orders = [
{ paid: true, total: 5 },
{ paid: false, total: 20 },
{ paid: true, total: 7 }
];
const total = orders
.filter(order => order.paid)
.reduce((sum, order) => sum + order.total, 0);
console.log(total);const orders = [
{ paid: true, total: 5 },
{ paid: false, total: 20 },
{ paid: true, total: 7 }
];
const total = orders
.filter(order => order.paid)
.reduce((sum, order) => sum + order.total, 0);
console.log(total);The filter keeps totals 5 and 7. The reducer starts with 0 and adds both, so the final total is 12.
Write the safe version and type what it prints.
// Buggy when the array is empty:
[].reduce((sum, n) => sum + n);
// Fix it by adding an initial value.console.log([].reduce((sum, n) => sum + n, 0));reduce((sum, n) => sum + n, 0) starts the accumulator at 0. Empty arrays now return 0 instead of throwing a TypeError.
Quiz: check your understanding
8 QUESTIONSAnswer once from your prediction, then read every explanation. The wrong answers are chosen to catch real bugs.
Question 1 of 8Which definition of a higher-order function is correct?
Choose an answer to see the explanation.
Question 2 of 8What does the callback lab print for doubled numbers?
Read the code, then predictfunction applyToEach(items, fn) { const results = []; for (const item of items) results.push(fn(item)); return results; } console.log(applyToEach([1, 2, 3], n => n * 2));Choose an answer to see the explanation.
Question 3 of 8Why is
applyToEach(numbers, double)different fromapplyToEach(numbers, double())?Choose an answer to see the explanation.
Question 4 of 8What does the multiplier factory print?
Read the code, then predictfunction makeMultiplier(n) { return value => value * n; } const triple = makeMultiplier(3); console.log(triple(5));Choose an answer to see the explanation.
Question 5 of 8What does
['1', '2', '3'].map(parseInt)print?Read the code, then predictconsole.log(["1", "2", "3"].map(parseInt));Choose an answer to see the explanation.
Question 6 of 8What happens when
reducehas no initial value on an empty array?Read the code, then predicttry { [].reduce((sum, n) => sum + n); } catch (error) { console.log(error.name); }Choose an answer to see the explanation.
Question 7 of 8Which version is more declarative?
Choose an answer to see the explanation.
Question 8 of 8What kind of higher-order function is
withLogging(fn)when it returns a new function that callsfn?Choose an answer to see the explanation.
Key takeaways
- A higher-order function takes a function as an argument, returns a function, or both.
- Pass the function value, such as
fn, when an API needs to call it later;fn()calls now. - Returned functions can remember factory settings through closures.
- Declarative pipelines can make intent clearer, but loops are still valid when they are clearer or more efficient.
- Array callback signatures matter:
mappasses element, index, and array, which explainsmap(parseInt). - Always consider an initial value for
reduce, especially when an array might be empty.
One-line definition.
A higher-order function is a function that accepts another function, returns another function, or both.
Up next: Pure functions & side effects, where you will separate calculations that always give the same result from code that changes the outside world.