Callbacks
Asynchronous callbacks, error-first callbacks in Node and why "callback hell" happens.
A callback is a function you pass to another function so it can call you back — either right away or later, when something has happened. Callbacks are the foundation of asynchronous JavaScript. Even though modern code mostly uses promises and async/await, you'll meet callbacks in event listeners, timers, array methods and older Node APIs, and understanding their limits explains why promises exist.
Synchronous callbacks#
You've already used plenty. Array methods call your function immediately, once per element:
Nothing asynchronous here — "after" waits for every callback to finish.
Asynchronous callbacks#
Other APIs store your callback and call it later, when an event happens or work completes:
Browser events are callbacks too: button.addEventListener("click", handleClick).
Writing your own callback-based function#
Let's simulate a database lookup that takes time:
The error-first convention
Node.js standardised error-first callbacks: the callback's first parameter is an error (or null), and the result comes after. You'll see it all over older Node code:
Rules of thumb for callback APIs:
- Always handle
errfirst andreturnso you don't continue with a missing result. - Call the callback exactly once.
- Be consistently async. A function that sometimes calls back immediately and sometimes later (sometimes called "releasing Zalgo") causes subtle ordering bugs.
Why try/catch doesn't help
That's why callback APIs pass errors as arguments instead of throwing them.
Callback hell#
Problems start when async steps depend on each other. Suppose we need to get a user, then their orders, then the first order's shipping status:
This "pyramid of doom" has real problems:
- Readability: the logic drifts right and is hard to follow.
- Error handling is repeated at every level — forget one and errors vanish.
- Running steps in parallel, or "whichever finishes first", needs manual counters and flags.
- Inversion of control: you hand your callback to someone else's code and trust them to call it once, with the right arguments, at the right time.
Taming it (a little)
Naming the steps flattens the pyramid:
It helps, but the fundamental issues remain. The real fix is promises.
From callbacks to promises#
Node can convert any error-first callback function into a promise-returning one:
And most Node modules already have promise versions, such as node:fs/promises:
Preview of where we're heading — the callback pyramid rewritten with async/await:
Flat, readable, with one place to handle errors.
Where callbacks are still the right tool#
- Events that happen many times:
addEventListener, Node'sstream.on("data", ...). Promises resolve only once. - Synchronous helpers:
map,filter,sortcomparators. - Configuration hooks:
onSuccess,onErroroptions in libraries.
Common mistakes#
- Forgetting to
returnafter calling back with an error, so the success code runs too. - Calling the callback twice (e.g. once in an error branch and again later).
- Calling the function instead of passing it:
setTimeout(done(), 100). - Expecting
try/catcharound an async call to catch errors thrown inside its callback.
What's next#
Next: promises — objects that represent a future value, and the foundation of modern asynchronous JavaScript.
Check your understanding
Quick quiz
1.In Node's error-first callback convention, what is the first argument to the callback?
2.What is "callback hell"?
3.Given
[3, 1, 2].forEach((n) => console.log(n)); console.log("done");, when is "done" printed?
Finished reading?
Mark this lesson complete to track your progress.