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.
- 01Spot higher-order functionsTell whether code takes a function, returns a function, or just calls one.
- 02Pass functions safelyGive a helper the function itself, not its result, and check which arguments the helper gives your function.
- 03Choose a loop or a pipelineDecide between a plain loop and array methods like
filterandmapby which one is easier to read.
The big idea
A higher-order function is a function that works with other functions. It does at least one of these:
- It takes a function as an argument. An argument is a value you give a function when you call it.
- It returns a function as its result.
Some do both. There is no special syntax for this. It works because in JavaScript a function is a value, like a number or a string. You can keep a function in a variable, put it in an object, give it to another function, and call it later.
You have already used this idea in the Callbacks, Arrow functions, Closures, and map, filter & reduce lessons. This lesson puts those pieces together. It is the first lesson of the Functional programming module.
A higher-order function splits a job into two parts. The helper does the part that is always the same, such as going through every item. The function you give it does the part that changes, such as what to do with each item.
A kitchen stand mixer has one motor. To whip cream you clip on a whisk. To knead bread you clip on a dough hook. You don't buy a new mixer for each food. You keep the motor and swap the tool.
- In real life: The mixer's motor and stand
- In JavaScript: The higher-order function, such as
applyToEachormap - In real life: The whisk, dough hook, or paddle you clip on
- In JavaScript: The function you pass in
- In real life: The motor turns the same way every time
- In JavaScript: The loop that is always the same
- In real life: Cream, dough, or batter from one machine
- In JavaScript: Different results from one helper
The lesson goes in this order:
- Functions are values.
- Passing a function into another function.
- Getting a function back from another function.
- Writing every step in a loop, or naming the jobs with array methods.
- A second look at
map,filter, andreduce.
Functions as values
When you write a function, you make a value. You can call that value with round brackets, but it is still a value. So 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 resultLine 1 makes a function called double. It takes a number n and returns n * 2.
Line 3 gives applyToEach the function itself. There are no call brackets after double. The helper keeps the function and calls it once for each item.
Line 4 has call brackets: double(). So double runs right away, before the helper starts, with no number. n is undefined, and undefined * 2 is NaN ("not a number"). The helper gets NaN. When it tries to call that, JavaScript throws a TypeError, because NaN is not a function.
double(), it gets the result of a call that already ran, so it has nothing to call.Code like this often looks almost the same but means something different. The table below shows three of them. Read each row as: "when I write this, it means this, and I use it for this."
| When you write | What it means | When you use it |
|---|---|---|
double | The function itself. Nothing runs yet. | You give it to a helper that will call it. |
double(3) | Call the function now with 3. You get 6. | You want the answer right now. |
() => double(3) | A new small function. When someone calls it, it calls double(3). | You want double(3) to run later, for example after a click. |
This small difference is the key to the whole lesson. Once you can hand a function to a helper, the helper can say: "I know how to repeat, wait, sort, or combine. You tell me what to do at each step."
Functions as arguments
INTERACTIVEA function that you pass into another function is called a callback. The two functions share the work:
- The higher-order function decides when to call the callback, and what to give it.
- The callback decides what happens each time it is called.
Try a helper called applyToEach. It takes a list of items and a callback. It goes through the items with a for...of loop, which runs once for each item. For each item it calls the callback and keeps what the callback returns. You can swap in a callback that doubles each number, turns it into a label, or checks if it is even.
applyToEach runs the same loop every time. The callback you pass in, here double, decides what each item becomes.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 the same. The function you pass in decides what each item becomes.
double turns each number into twice the number. The loop stays the same; only the callback changes.
double(), the function would run right away, and the helper would get its result instead.The array methods you know work the same way. map, filter, reduce, and sort all take a callback. So does addEventListener, which runs your function when something happens on the page, such as a click. These methods differ in two ways:
- What they give to the callback.
- What they do with the value the callback returns.
Array methods give their callback more than one argument. Usually they give three: the item, its position (the index, starting at 0), and the whole array. Your callback can ignore the ones it doesn't need. But if your callback uses its second argument for something else, it will get the index there anyway.
const words = ["1", "2", "3"];console.log(words.map(parseInt));console.log(words.map(word => parseInt(word, 10)));parseInt turns text into a whole number. It takes an optional second argument: the number base, such as 10 for normal numbers or 2 for binary.
Line 2 prints [1, NaN, NaN]. Here is why. map calls its callback as callback(item, index, array). So parseInt gets the index as its number base:
parseInt("1", 0)gives 1. A base of 0 means "work it out", so it uses base 10.parseInt("2", 1)givesNaN. Base 1 doesn't exist.parseInt("3", 2)givesNaN. Base 2 only has the digits 0 and 1.
Line 3 fixes it with a small arrow function: word => parseInt(word, 10). Now parseInt only gets the word and the base 10, and it prints [1, 2, 3].
A higher-order function can also take a function and return a new one that does a little more. That new function is called a wrapper. It adds some work before or after the original function, like a gift wrap around a present. The present inside is unchanged.
const cart = {
taxRate: 0.1,
total(price, quantity) {
return price * quantity * (1 + this.taxRate);
}
};
function withLogging(fn) {
return function (...args) {
console.log("args", args.join(", "));
const result = fn.apply(this, args);
console.log("result", result);
return result;
};
}
cart.total = withLogging(cart.total);
console.log(cart.total(10, 2));Here is the code, step by step:
- Lines 1 to 6 make a
cartwith a tax rate of 0.1 and atotalmethod. Inside the method,thismeans the cart, sothis.taxRateis 0.1. - Line 9:
...argscollects every argument the wrapper gets into one array, calledargs. - Line 10 prints the arguments.
- Line 11 calls the original function with
fn.apply(this, args).applycallsfnwith the samethis(the cart) and the same arguments. - Lines 12 and 13 print the result and return it.
- Line 17 replaces
cart.totalwith the wrapped version. Line 18 calls it.
It prints args 10, 2, then result 22, then 22. The original method still reads the cart's tax rate (10 × 2 × 1.1 = 22), and the wrapper returns its result unchanged.
function withTiming(fn, label) {
return (...args) => {
console.log(label + " start");
const result = fn(...args);
console.log(label + " end");
return result;
};
}
const saveProfile = withTiming(
name => "saved " + name,
"saveProfile",
);
console.log(saveProfile("Asha"));withTiming prints saveProfile start, calls the original function, prints saveProfile end, and returns the result. So the last line prints saved Asha.
A real timing wrapper reads performance.now() before and after the call. That method returns the current time in milliseconds, so the wrapper can print how long the call took. Every wrapper follows the same rules:
- Add your work before or after the call.
- Call the original function once.
- Pass on all its arguments.
- Return its result.
If a wrapper forgets the last rule, every call through it returns undefined, and no error tells you why.
Returning functions
STEP THROUGHA higher-order function can also make a new function and return it. A function that does this is called a function factory. You call the factory once with a setting. It gives you back a function that you can call many times.
You order a rubber stamp that says "Hi". You tell the shop what to write only once. After that, you press the stamp on every card. You don't explain "Hi" again each time.
- In real life: Tell the shop what the stamp should say, once
- In JavaScript: Call
makeGreeter("Hi")ormakeMultiplier(3) - In real life: Get a stamp back
- In JavaScript: Keep the returned function in a variable
- In real life: Press it many times
- In JavaScript: Call the returned function many times
- In real life: Each stamp keeps its own design
- In JavaScript: Each returned function keeps its own setting
Step through the factory below. Pick a factor, then watch the returned function keep using that factor in later calls. The steps were recorded from the lesson's real functions. It is a guided replay, not the browser's own debugger.
Pick a number for the factory and predict both results. Then step through the factory call and the two later calls to the returned function.
script
function makeMultiplier(factor) { return function (value) { return value * factor; };} multiplier(4);multiplier(10);factor is 3, and you can call that function as often as you like.How can the returned function still read factor after makeMultiplier has finished? It is a closure: a function that remembers the variables around the place where it was made. JavaScript keeps factor alive because the returned function still needs it. The Closures and Closure patterns lessons explain how that memory works.
The block below shows four common functions that return functions:
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)makes a function that multiplies by 3, ready to use again and again.makeCounter()makes a counter whosecountno other code can touch.once(fn)makes a function that runsfnonly the first time.withLogging(fn)makes a function that prints, then callsfn.
Each returned function keeps the values from the call that made it: the multiplier's n, the counter's count, and once's called and result.
Declarative vs imperative
COMPAREThere are two ways to write the same job:
- Imperative code spells out every step: make a list, go through each item, check it, add it.
- Declarative code names the jobs you want done, such as "keep the paid orders", and lets a helper like
filterdo the steps.
Higher-order functions often make code more declarative. A chain of array methods, where each method passes its result to the next, is called a pipeline.
At a restaurant you say what you want: "A large pizza, no onions." You don't tell the cook how to knead the dough or how hot the oven should be. The cook still does all those steps. You just don't have to write them out.
- In real life: Say: large pizza, no onions
- In JavaScript: Write
orders.filter(...).reduce(...) - In real life: Tell the cook every chopping and baking step
- In JavaScript: Write every loop,
if, push, and addition - In real life: The kitchen still does every step
- In JavaScript: The array methods still loop inside
Below, the same small report is written both ways. Both keep the paid orders, make a label for each, and add up their totals. The lab also counts how many times each version visits an order. That count shows that the declarative version doesn't always do less work.
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 plain loop can still be clearer when there are many branches, when you stop early, or when the code runs very often.
The pipeline names the jobs: keep the paid orders, make labels, add up a total. Here it visits orders more times, but the rule is easy to read.
Sometimes a plain loop is the clearer choice. This loop looks for the first price above 5:
const prices = [4, 7, 6];
let firstExpensive;
for (const price of prices) {
if (price > 5) {
firstExpensive = price;
break;
}
}
console.log(firstExpensive);break stops a loop at once. The loop finds 7, keeps it, and stops, so it never looks at 6. It prints 7.
You could write prices.filter(price => price > 5)[0] instead. It gives 7 too. But it checks every price and builds a new array first, and the code doesn't show that you only wanted the first match.
Choose a loop when you need:
- to stop early with
break, - several variables that change as you go,
- many branches of
ifandelse, or - code that runs very often, where extra arrays slow things down.
Choose map, filter, and reduce when their names make the job easier to read.
In the end, pick the version a teammate can safely change next month. For "keep, change, combine", a pipeline reads like the rule itself. For many branches or stopping early, a loop shows what really happens.
map, filter & reduce revisited
EDGE CASESThe earlier array lesson showed what each of these methods does. Now look at them as higher-order functions. Each one takes a callback:
mapcalls its callback for each item and returns a new array of the results.filtertakes a callback that returnstrueorfalse. It returns a new array of the items where the callback returnedtrue.reducetakes a callback that combines items one by one into a single result. The result so far is called the accumulator.reducereturns the final accumulator.
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));Here is what each part does:
- Lines 7 to 10 count the orders by status. The accumulator
groupsstarts as an empty object,{}.??means "use 0 if there is no count yet". Line 22 prints{ paid: 2, draft: 1 }. - Lines 12 to 15 copy what
mapdoes. They start with an empty array and push each number doubled. Line 23 prints[2, 4, 6]. - Lines 17 to 20 copy what
filterdoes. They push only the even numbers. Line 24 prints[2, 4]. - Line 25 calls
reduceon an empty array with no starting value. It throws an error.
So reduce can do the job of map and filter. That is good to know, but it is harder to read. When map or filter already says what you mean, use it.
If you give reduce no starting value, it uses the first item as the starting accumulator. An empty array has no first item. So [].reduce((a, b) => a + b) throws a TypeError. When the array might be empty, pass a starting value as the second argument, such as 0.
Remember what each method gives its callback. Array callbacks get the item, the index, and the array. That is why arr.map(parseInt) goes wrong. Other methods give something else. The callback for sort gets two items, (a, b), and returns a number that says which comes first.
Where you’ll use this
You will meet higher-order functions in everyday frontend code:
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)}/>;- Line 1: an event handler. The browser keeps
handleSaveand calls it when the click happens. - Lines 3 to 5: an array pipeline. It keeps the active users, then takes their names for the page.
- Line 7: a wrapper. On a server, middleware is a function that runs before the code that answers a request. Wrappers like
withLoggingadd logging, login checks, timing, or retries around it. - Lines 9 to 12: a React-style component. It gets a callback,
onSelect, from its parent. When the user picks a product, the component callsonSelect. It doesn't need to know what the parent does next.
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 line by what it does: takes a function, returns a function, does both, or does neither.
Common misconceptions
"Higher-order means complicated."
The rule is small: it takes a function, or it returns one. Code gets hard to read for other reasons: unclear callback names, callbacks that secretly change things, or a clever chain where a plain loop would be clearer.
"A callback and a higher-order function are the same thing."
The callback is the function you pass in. The higher-order function is the one that takes it, returns it, or wraps it.
"A pipeline always does less work."
A chain can go through the items several times, once per method. That is usually fine. But for code that runs very often, one loop can be faster.
"reduce is the advanced choice for everything."
reduce can build almost anything, but other methods say what you mean more clearly. Use map to get one new item for each item. Use filter to keep some items and drop others.
"Every function from one factory shares the same settings."
Each call to the factory makes a new function with its own remembered values. If you copy that function into another variable, both names use the same function and the same values. But calling the factory again makes a fresh function with fresh values.
The table below keeps four similar ideas apart. Read each row as: "this idea is this, it looks like this in code, and this is the mistake to avoid."
| Which idea | What it is | What it looks like | What to watch out for |
|---|---|---|---|
| Callback | A function you pass in, so a helper can call it later or many times. | items.map(fn) | Pass fn, not fn(). |
| Function factory | A function that makes and returns a new function. | makeMultiplier(3) | Each call makes a new function with its own remembered values. |
| Wrapper | A function that takes a function and returns a new one that calls it, with extra work around it. | withLogging(save) | Return the original result, unless you really mean to change it. |
| Pipeline | A chain of array methods that names each job instead of writing every loop step. | orders.filter(...).map(...) | Too many tiny steps can make simple code hard to follow. |
Practice exercises
5 EXERCISESRead the helper and predict the exact array it prints.
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 then for 4. It keeps both results in an array, so the console prints [3, 5].
Paste the program into your console. Then change both numbers 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 is 4. Calling times4(6) returns 24.
Use the wrapper to check code that must run only once, such as sending a receipt or setting up a widget. What are the three printed lines?
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. It adds 1 to sends, keeps the result 1, and sets called to true. The second call skips the original and returns the kept 1. sends is still 1. So the three lines are 1, 1, 1.
Say why the pipeline reads as "keep the paid orders, then add them up". 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 the orders with totals 5 and 7. The reducer starts at 0 and adds both, so the 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. An empty array now gives 0 instead of throwing a TypeError.
Quiz: check your understanding
8 QUESTIONSPick your answer first, then read every explanation. The wrong answers are mistakes people really make.
Question 1 of 8Which sentence says what a higher-order function is?
Choose an answer to see the explanation.
Question 2 of 8What does this code print?
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 8How is
applyToEach(numbers, double)different fromapplyToEach(numbers, double())?Choose an answer to see the explanation.
Question 4 of 8What does this factory code 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 you call
reduceon an empty array with no starting value?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, that is, names the jobs instead of spelling out every step?
Choose an answer to see the explanation.
Question 8 of 8
withLogging(fn)returns a new function that callsfn. What kind of higher-order function is it?Choose an answer to see the explanation.
Key takeaways
- A higher-order function takes a function as an argument, returns a function, or does both.
- Pass
fnwhen a helper should call it later.fn()calls it now. - A returned function remembers the factory's settings. That memory is a closure.
- A pipeline can make the jobs easy to read. A loop is still fine when it is clearer or faster.
- Array callbacks get the item, the index, and the array. That is why
map(parseInt)goes wrong. - Give
reducea starting value, especially when the array might be empty.
One-line definition.
A higher-order function is a function that takes another function, returns another function, or both.
Up next: Pure functions & side effects. You will learn to tell apart functions that always give the same result for the same input from code that changes things outside itself.