In JavaScript, functions are values. You can store a function in a variable, put it in an array, pass it to another function, and even return it from a function, exactly as you would with a number or a string. Programmers call this having first-class functions, and it's one of the most powerful ideas in the language.
A function that you pass into another function so it can be called later is a callback. You've already used callbacks without the name: the function you give to addEventListener runs when the button is clicked, the one you give to setTimeout runs after the delay, and the one you give to map runs once for each array item. A function that takes or returns other functions is called a higher-order function.
In this lesson you'll write your own callbacks and higher-order functions, learn how closures let a returned function remember variables, and see why deeply nested callbacks led to Promises. These ideas sit underneath almost everything in modern JavaScript, from events to React.
Functions are values
const greet = function (name) { return `Hi ${name}`; };
const shout = (text) => text.toUpperCase();
const tools = [greet, shout];
console.log(tools[1]("hello")); // "HELLO"- A function name without brackets,
greet, refers to the function itself. - With brackets,
greet("Sam"), it calls the function and gives you the result. - Mixing these up is the most common callback mistake: pass
greet, notgreet().
Writing a function that takes a callback
function repeat(times, action) {
for (let i = 1; i <= times; i++) {
action(i);
}
}
repeat(3, (n) => console.log("Step", n));repeat doesn't know or care what action does. It just calls it at the right moment with the right argument. This separation is why callbacks are so useful: the same repeat can log, draw, or save data, depending on the callback you pass. Array methods work the same way: numbers.filter(n => n > 10) calls your function for each item and keeps the ones where it returns true.
Functions that return functions, and closures
function multiplier(factor) {
return (n) => n * factor;
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5), triple(5)); // 10 15The returned function remembers factor even after multiplier has finished. That memory is called a closure. Closures let you create configured functions, keep private state such as a counter, and they're the reason event handlers can still use the variables around them long after the setup code has run. You met the idea of where variables live in the scope lesson.
Synchronous and asynchronous callbacks
| Kind | When the callback runs | Examples |
|---|---|---|
| Synchronous | Immediately, before the outer function returns | map, filter, forEach, sort |
| Asynchronous | Later, after something happens | setTimeout, addEventListener, older file and network APIs |
When asynchronous steps depend on each other, callbacks get nested inside callbacks, creating a pyramid shape known as callback hell. It's hard to read and harder to handle errors in. Promises and async/await were created to flatten that pyramid.
JavaScript Callbacks Syntax
Callbacks and closures
function run(cb) { cb("done"); }
run(msg => console.log(msg));
const nums = [1, 2, 3].map(n => n * 10);
button.addEventListener("click", handleClick);
function makeCounter() {
let count = 0;
return () => ++count;
}JavaScript Callbacks Summary
- Functions are values: you can store, pass and return them.
- A callback is a function passed to another function to be called later.
- A higher-order function takes or returns functions, like
map,filterandaddEventListener. - Pass the function (
fn), not its result (fn()). - A closure is a function that remembers variables from where it was created.
JavaScript Callbacks Examples
Each example runs in the page. Edit the code and press Run to see what changes.
Your own higher-order function
function processOrder(order, onSuccess, onError) {
if (order.items.length === 0) {
onError("Basket is empty");
return;
}
const total = order.items.reduce((sum, item) => sum + item.price, 0);
onSuccess(total);
}
const showTotal = (total) => console.log(`Charged £${total.toFixed(2)}`);
const showError = (message) => console.log("Error:", message);
processOrder({ items: [{ price: 4.5 }, { price: 2 }] }, showTotal, showError);
processOrder({ items: [] }, showTotal, showError);Line-by-line explanation
- Line 1The function receives two callbacks, one for success and one for failure.
- Line 3On a problem, it calls the error callback.
- Line 6
reduceitself takes a callback to add up the prices. - Line 7On success, it calls the success callback with the result.
- Lines 13 to 14The same function, two outcomes, depending on the data.
Closures that remember
function makeCounter(start = 0) {
let count = start;
return {
increment: () => ++count,
reset: () => (count = start),
};
}
const likes = makeCounter();
const views = makeCounter(100);
likes.increment();
likes.increment();
views.increment();
console.log("likes:", likes.increment());
console.log("views:", views.increment());
console.log("reset likes:", likes.reset());Line-by-line explanation
- Line 2
countis private: nothing outside can change it directly. - Lines 4 to 5Both functions close over the same
count. - Lines 9 to 10Each call to
makeCountercreates a separate closure with its own count. - Lines 15 to 16Likes reaches 3 and views reaches 102 independently.
Callback hell and the way out
function step(name, delay, next) {
setTimeout(() => { console.log(name); next(); }, delay);
}
step("1. Fetch user", 100, () => {
step("2. Fetch orders", 100, () => {
step("3. Fetch invoice", 100, () => {
console.log("All done, but look at that indentation");
});
});
});
const wait = (ms) => new Promise(r => setTimeout(r, ms));
(async () => {
await wait(400);
console.log("With async/await the same steps read top to bottom");
})();Line-by-line explanation
- Lines 1 to 3A function that does something later, then calls the next step.
- Lines 5 to 11Each step nests inside the previous one, creating the pyramid.
- Lines 13 to 17Promises and
awaitflatten the same idea into straight lines.
Common JavaScript Callbacks Mistakes
Calling the callback too early
button.addEventListener("click", save()); // save runs now, not on clickbutton.addEventListener("click", save);
button.addEventListener("click", () => save("draft"));Forgetting to return in a callback
const doubled = [1, 2, 3].map(n => { n * 2 }); // [undefined, undefined, undefined]const doubled = [1, 2, 3].map(n => n * 2);With curly braces, an arrow function needs an explicit return.
Losing the remove handle
el.addEventListener("scroll", () => update());
el.removeEventListener("scroll", () => update()); // a different function, nothing removedconst onScroll = () => update();
el.addEventListener("scroll", onScroll);
el.removeEventListener("scroll", onScroll);JavaScript Callbacks in Real Projects
Callbacks everywhere
Every event listener in the browser is a callback. Node.js was built around callbacks for file and network access before Promises arrived. React components pass callbacks such as onClick and onChange down to child components. Libraries like Lodash provide higher-order helpers such as debounce, which take your function and return a new, improved one.
JavaScript Callbacks Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Pass logItem as the callback to forEach without calling it.
function logItem(item) { console.log(item); }
["a", "b"].forEach();
What is logged?
function make(x) {
return (y) => x + y;
}
const add5 = make(5);
console.log(add5(2), make(1)(1));What is a callback?
What is a closure?
applyToAll should return a new array with fn applied to every item, and addTax should be a function returned by withTax that adds 20%. Fix both.
function applyToAll(items, fn) {
const out = [];
for (const item of items) out.push(fn);
return out;
}
function withTax(rate) {
(price) => price * (1 + rate);
}
const addTax = withTax(0.2);
console.log(applyToAll([10, 20], addTax));- withTax returns a function
- Tax is added
- applyToAll calls the callback for each item
JavaScript Callbacks Quiz
5 questions to check what you learned.
Which is a higher-order function?
- Math.PI
- array.map
- console
- "text".length
What's wrong with setTimeout(alertUser(), 1000)?
- Nothing
- The delay is too short
- setTimeout needs a string
- alertUser runs immediately and its result is passed instead
What is callback hell?
- An error message
- Deeply nested callbacks that are hard to read and maintain
- A callback that never runs
- A browser setting
Does the callback passed to array.map run synchronously?
- Yes, once per item, before map returns
- No, it runs later
- Only for the first item
- Only in Node.js
Why does each call to makeCounter() keep its own count?
- Counters are global
- JavaScript copies the function
- Because of let
- Each call creates a new scope, and the returned functions close over that scope
JavaScript Callbacks Challenge
Build a tiny event system
Write createEmitter() that returns an object with:
on(name, callback): stores the callback under the event name (several callbacks per name)emit(name, data): calls every callback for that name withdata, in the order they were addedcount(name): returns how many callbacks are stored for that name (0 if none)
function createEmitter() {
}
const shop = createEmitter();
shop.on("sale", item => console.log("Sold:", item));
shop.on("sale", item => console.log("Update stock for", item));
shop.emit("sale", "mug");- Callbacks run with the data
- Other events are unaffected
- count reports stored callbacks