Callbacks & Higher-Order Functions
A callback is a function you hand to another function so it can call it later. A higher-order function is one that takes functions as arguments or returns them. This idea — passing behaviour around like data — is at the heart of array methods, event handling and asynchronous JavaScript.
A callback, step by step
function repeat(times, action) { for (let i = 1; i <= times; i++) { action(i); } } repeat(3, n => console.log(`Step ${n}`));
Step 1 Step 2 Step 3
repeat knows how many times to do something; the callback decides what to do. Separating those two concerns is what makes higher-order functions so reusable.
Callbacks you already know
| API | The callback is called… |
|---|---|
array.forEach(fn), map, filter… | Once for each element |
array.sort((a, b) => …) | To compare pairs of elements |
element.addEventListener("click", fn) | Each time the event happens |
setTimeout(fn, ms) | Once, after a delay |
promise.then(fn) | When an asynchronous result arrives |
Synchronous vs asynchronous callbacks
Some callbacks run immediately, before the higher-order function returns (map, forEach). Others run later (setTimeout, events, network responses):
console.log("1. start"); [1, 2].forEach(n => console.log(`2. sync callback ${n}`)); setTimeout(() => console.log("4. async callback"), 0); console.log("3. end");
1. start 2. sync callback 1 2. sync callback 2 3. end 4. async callback
Even with a delay of 0, the timer callback waits until the current code has finished. Lesson 51 explains why, with the event loop.
Writing your own higher-order functions
Here's a simplified version of filter, to show there's no magic involved:
function keep(items, test) { const result = []; for (const item of items) { if (test(item)) result.push(item); } return result; } const words = ["sun", "moonlight", "sky", "starfield"]; console.log(keep(words, w => w.length > 3)); console.log(keep([5, 12, 8, 21], n => n % 2 === 0));
[ 'moonlight', 'starfield' ] [ 12, 8 ]
Functions that return functions
Higher-order functions can also produce new functions. These small helpers combine with array methods nicely:
const greaterThan = min => n => n > min; const property = key => obj => obj[key]; const scores = [45, 82, 67, 91]; console.log(scores.filter(greaterThan(60))); const people = [{ name: "Ida" }, { name: "Joe" }]; console.log(people.map(property("name")));
[ 82, 67, 91 ] [ 'Ida', 'Joe' ]
Composing functions
Composition chains small functions into a pipeline, where each function's output feeds the next:
const pipe = (...fns) => input => fns.reduce((value, fn) => fn(value), input); const trim = s => s.trim(); const lower = s => s.toLowerCase(); const dashes = s => s.replace(/\s+/g, "-"); const slugify = pipe(trim, lower, dashes); console.log(slugify(" Hello Big World "));
hello-big-world
Callback pitfalls
Calling instead of passing
function sayDone() { return "done"; } setTimeout(sayDone, 0); const wrong = sayDone(); console.log(typeof wrong);
string
setTimeout(sayDone(), 0) would call sayDone immediately and pass its result (a string) as the "callback". Pass the function itself, without parentheses.
Unexpected extra arguments
console.log(["1", "2", "3"].map(parseInt)); console.log(["1", "2", "3"].map(s => parseInt(s, 10)));
[ 1, NaN, NaN ] [ 1, 2, 3 ]
map calls its callback with (item, index, array). parseInt treats the second argument as the number base, so parseInt("2", 1) is NaN. Wrapping in an arrow function controls exactly which arguments are passed.
Callback hell
Before promises, a sequence of asynchronous steps meant callbacks nested inside callbacks inside callbacks — a "pyramid of doom" that was hard to read and harder to handle errors in. Promises and async/await (lessons 52–53) were designed to fix that.