All topics
Functionsbeginner

Callbacks and Callback Hell

Passing a function to be executed later, and why deeply nested callbacks became a maintainability problem promises and async/await later solved.

A callback is simply a function passed as an argument to another function, intended to be invoked at some later point — either synchronously (like in array.forEach(callback)) or asynchronously (like in setTimeout(callback, 1000) or a network request's completion handler). Callbacks were JavaScript's original mechanism for asynchronous programming, and while they've been largely superseded by promises and async/await for async work, understanding them is essential since so many APIs (event listeners, array methods) still use the callback pattern directly.

A callback is like leaving your phone number with a restaurant host and going to wait at the bar — instead of standing in line asking 'is my table ready?' over and over (blocking), you get called back the moment it's ready, and can drink at the bar in the meantime (non-blocking).

Key Concepts

1
The practical problem callbacks solve is sequencing work that depends on something happening first, especially something that takes unknown time like a network request or a timer, without blocking the single JS thread while waiting. Instead of blocking, you register a callback and the runtime calls it once the operation completes, letting other code run in the meantime.
2
The well-known downside, nicknamed 'callback hell' or the 'pyramid of doom,' emerges when multiple asynchronous operations must happen in sequence, each depending on the previous one's result. Nesting callback inside callback inside callback produces code that drifts rightward with each level, becomes hard to read, and makes error handling repetitive since every level needs its own error-checking branch. Debugging stack traces in deeply nested callbacks is also notoriously painful because the original calling context is lost.
3
Promises, and later async/await built on top of them, were introduced specifically to flatten this nesting back into sequential-looking code while preserving the same non-blocking behavior. Even so, plain callbacks remain the right tool for simple, one-off event handling (button.addEventListener('click', callback)) where there's no chain of dependent async steps to manage.
button.addEventListener('click', callback)