Topic 3.7
Callbacks and Higher-Order Functions
In one line
In JavaScript, functions are values: you can store them, pass them to other functions (callbacks) and return them; a higher-order function is one that takes or returns a function, like map, filter, addEventListener and setTimeout.
Think of it like this
Leaving your phone number with a shop. You say: "when the shoes in my size arrive, call me back". You hand over *how to reach you* (a callback), and the shop decides *when* to use it. The shop is like a higher-order function: its job involves calling a function someone else gave it.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- First-class function
- A function that can be used like any other value: stored, passed and returned.
- Callback
- A function you pass to another function so it can call it later or for each item.
- Higher-order function
- A function that takes a function as input, returns a function, or both.
- Synchronous
- Runs now, in order, before the next line continues.
- Asynchronous
- Runs later, after the current code has finished, for example when a timer fires or a click happens.
- Radix
- The number base, like 10 for normal numbers or 2 for binary.
parseInt's second parameter. - Wrapper / decorator
- A function that takes a function and returns a new one with extra behaviour around it.
- Declarative
- Code that says what result you want (
filter(isCheap)), not every step of how to get it.
Step by step
01Store a function in a variable
A function name without () is just a value. You can give it another name and call it through that name.
function shout(text) {
return text.toUpperCase() + "!";
}
const yell = shout; // no () : the function itself
console.log(yell("hi")); // "HI!"02Pass it to another function
speak doesn't know how to style text. It takes a styling function and calls it. Swap the callback and you swap the behaviour.
function speak(text, style) {
return style(text); // call the callback
}
speak("Hello", shout); // "HELLO!"
speak("Hello", t => t + "?"); // "Hello?"03Use the built-in higher-order functions
map, filter and reduce take a callback and do the looping for you.
const prices = [120, 45, 300, 80];
prices.map(p => p * 2); // [240, 90, 600, 160]
prices.filter(p => p < 100); // [45, 80]
prices.reduce((sum, p) => sum + p, 0); // 54504Know when the callback runs
Array callbacks run immediately. Timer and event callbacks run later, after the current script finishes.
05Return a function
A wrapper takes a function and returns a new function that adds something around each call.
function logged(fn) {
return function (...args) {
console.log("calling with", args);
return fn(...args);
};
}
const loggedAdd = logged((a, b) => a + b);
loggedAdd(2, 3); // logs: calling with [ 2, 3 ], returns 5Try it yourself
- 1
Write your own style
In the first example, write a
robotfunction that puts a space between every letter (hint:text.split("").join(" ")) and pass it tospeak. - 2
Chain them
In the second example, write one line that doubles every price, keeps only results under 200, then adds them up. Predict the number first.
- 3
Reorder the output
In the third example, change the timer's delay from
0to1000and set nothing else. Does the order change? Why not?
Code & diagrams
Console
(nothing was printed)Expected console output
HELLO!
hello...
Hello?
2 HI!
step 1
step 2
step 3`sort()` without a callback compares items as strings, so `"100"` comes before `"20"`. A compare callback fixes it.
Console
(nothing was printed)Expected console output
[ 240, 90, 600, 160 ]
[ 45, 80 ]
545
2 cheap items
[ '0:a', '1:b' ]
[ 1, NaN, NaN ]
[ 1, 2, 3 ]
[ 1, 2, 3 ]
[ 100, 20, 3 ]
[ 3, 20, 100 ]The forEach callback runs during the script; the timer callback waits until the script ends, even with a 0 ms delay. `once` is a higher-order function that returns a wrapped function.
Preview
Console
Nothing logged yet.Expected console output
1. script starts
2. forEach callback, item 1
2. forEach callback, item 2
4. script ends
5. timer callback runs laterBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Pass the result instead of the function
Write setTimeout(showMessage(), 1000);.
Break #2
Pass something that isn't a function
Write [1, 2, 3].map("double").
Myth vs fact
Myth
Callbacks are always asynchronous.
Fact
Many run synchronously, like the ones passed to map, filter and forEach. Only APIs like timers, events and network requests call them later.
Myth
setTimeout(fn, 0) runs fn immediately.
Fact
It runs fn only after the current script (and other queued work) has finished, never in the middle of it.
Myth
Higher-order functions are slower than loops, so avoid them.
Fact
For normal-sized data the difference is negligible and engines optimise them well. Prefer the clearer code; measure before optimising hot paths.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Callback-heavy async code nests deeply ("callback hell") and makes error handling awkward; Promises and
async/await(the async phase) were designed to flatten it. Node's older APIs use the error-first callback convention:fn(err, result). - ▸
map,filterandforEachskip holes in sparse arrays and readlengthonce at the start; items appended during iteration aren't visited.Array.prototype.sortis guaranteed stable since ES2019. - ▸
Passing a method as a callback loses its
this:setTimeout(obj.method, 0)calls it withthisset to the global object (sloppy) orundefined(strict). Use an arrow (() => obj.method()) orobj.method.bind(obj). - ▸
Creating a new arrow inside a render loop or on every call is cheap for the engine, but in UI libraries it changes function identity each time, which can defeat memoisation (React's
useCallbackexists for this).
Remember this
- 1
Functions in JavaScript are first-class values (also called first-class citizens): anything you can do with a number or a string, you can do with a function. Store it in a variable (
const f = greet;), put it in an array or object, pass it as an argument, and return it from another function. Under the hood a function is a special kind of object, which is why it can have properties likenameandlength. - 2
A callback is a function you pass to another function so that function can call it ("call it back") at the right moment. You pass the function itself, without parentheses:
button.addEventListener("click", handleClick). WritinghandleClick()would run it right now and pass its result instead (the bug from Topic 3.1). - 3
A higher-order function (HOF) is a function that takes a function as an argument, returns a function, or both. You already know some:
addEventListenerandsetTimeouttake callbacks;makeCounterandmultiplierfrom Topic 3.6 return functions. The array methods are the everyday HOFs:forEach(fn)runsfnon each item,map(fn)builds a new array of results,filter(fn)keeps items wherefnreturns something truthy,reduce(fn, start)folds a list into one value, andsort(compareFn)uses your function to compare pairs (the arrays phase covers them all in depth). - 4
Callbacks can run synchronously or asynchronously. A synchronous callback runs right away, before the HOF returns:
mapandforEachcall your function for every item and only then move on. An asynchronous callback runs later, after the current code has finished:setTimeout(fn, 0)and event listeners handfnto the browser, which calls it when the timer fires or the click happens. Even a0ms timer waits until the current script is done (the event loop, covered in the async phase). - 5
When a HOF calls your callback, it decides which arguments to pass.
mappasses three: the item, its index and the whole array. That's harmless for most callbacks, but surprising when the callback accepts optional extra parameters:["1", "2", "3"].map(parseInt)gives[1, NaN, NaN], becauseparseIntreceives the index as its second parameter, the radix (number base):parseInt("2", 1)isNaN. Wrap it to control the arguments:.map(s => parseInt(s, 10)), or useNumber. - 6
Returning functions lets you build behaviour from parts. A function that wraps another adds a feature around it without changing it:
once(fn)runsfnat most once,logged(fn)prints every call,memoize(fn)caches results. This is called a decorator or wrapper. Combined with closures, HOFs let you write code that says *what* to do (prices.filter(isCheap)) instead of spelling out *how* with loops and counters every time. This style is called declarative and is the heart of functional programming.
Explain it without notes
What does it mean that functions are first-class in JavaScript?
What is a callback, and what is a higher-order function? Give two built-in examples of each idea.
What is the difference between a synchronous and an asynchronous callback?
Why does ["1", "2", "3"].map(parseInt) return [1, NaN, NaN]?
Practice
Write applyTwice(fn, value) that returns fn(fn(value)), and test it with n => n + 3 and 5.
Using filter and map, turn ["apple", "kiwi", "banana", "fig"] into the upper-case names of fruits longer than 3 letters.
Write logged(fn) that returns a new function which prints "result: " and the result each time, and returns the result.
Trade-offs
- ↔
Chains like
filter().map().reduce()read clearly but loop over the data several times and create intermediate arrays; a singleforloop is faster for very large arrays in hot code. - ↔
Inline arrows keep logic next to where it's used; named functions can be reused, tested on their own and show up by name in stack traces.
Done when you can
I can store, pass and return functions.
I can pass a callback without accidentally calling it.
I can use
map,filterandreducewith arrow callbacks.I can say whether a callback runs now or later.
I can write a higher-order function that wraps another function.