Callbacks, Callback Hell & Error-First Pattern
Pehle sirf callbacks the — function do, baad mein call karunga. 3 nested = pyramid of doom. Phir Promises aaye aur sab theek hua.
Callback = a function passed as an argument, to be called later. You hand someone a function and say "call this when you're done."
Synchronous callback: array.map(callback) — called immediately in the same call stack. The thread is blocked until it returns.
Asynchronous callback: setTimeout(callback, 1000) — called later in a different call stack turn. The thread is free while waiting.
The key difference: Sync callbacks block the thread. Async callbacks yield control back to the event loop.
ALL JavaScript async is built on callbacks at the lowest level — even Promises and async/await compile down to callbacks. Web APIs (setTimeout, fetch, addEventListener) accept callbacks because they operate on separate browser threads.
// Synchronous callback — runs NOW
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2); // map calls callback immediately
console.log(doubled); // [2, 4, 6] — same call stack
// Asynchronous callback — runs LATER
console.log("1");
setTimeout(() => console.log("2"), 0); // Callback queued for later
console.log("3");
// Output: 1, 3, 2 — even with 0ms delay!
// Why? setTimeout queues the callback as a macrotask.
// Current call stack must clear first (1, 3), THEN callback runs (2).
// Event listener callback — runs on user action
button.addEventListener("click", function handleClick() {
// This callback runs when user clicks — could be seconds later
console.log("Button clicked!");
});Callback hell = deeply nested callbacks where each async step depends on the previous result. Also called the "pyramid of doom" because indentation forms a pyramid shape pointing right.
Problem 1: Hard to read — deeply nested, flow goes top-to-bottom-to-right, not linear. Your eyes zigzag across the screen.
Problem 2: Hard to debug — stack traces show "anonymous" for every callback. Where did the error originate? Good luck finding out.
Problem 3: Error handling is painful — must check for errors at EVERY level. Forget one check and your code proceeds with undefined data.
Problem 4: Inversion of control — you give your callback to someone else. What if they call it twice? Never? With wrong args?
Real example: getUser → getOrders → getOrderDetails → processPayment → sendEmail — 5 levels deep, 5 error checks.
// Callback hell — 4 async steps, each depends on previous
getUser(userId, (err, user) => {
if (err) { handleError(err); return; }
getOrders(user.id, (err, orders) => {
if (err) { handleError(err); return; }
getOrderDetails(orders[0].id, (err, details) => {
if (err) { handleError(err); return; }
processPayment(details.total, (err, receipt) => {
if (err) { handleError(err); return; }
sendEmail(user.email, receipt, (err) => {
if (err) { handleError(err); return; }
console.log("Done! Receipt:", receipt.id);
});
});
});
});
});
// 5 levels deep. Error check at EVERY level. Inversion of control on every call.
// This is the "pyramid of doom". Hard to read, hard to debug, hard to maintain.
Error.stack only shows the current callback's frames — not the chain of callbacks that led to it. This is WHY Promises were created — they preserve the error chain across async boundaries.Node.js convention: callback's first argument is ALWAYS the error (null if success), second argument is the result.
Pattern: function callback(err, result) — if err is truthy, something went wrong.
This pattern standardizes error handling but still requires checking at every level. Many Node.js APIs use this: fs.readFile, http.get, crypto.pbkdf2.
The problem: if you forget to check err, your code silently proceeds with undefined result — and crashes later in unpredictable ways.
// Error-first callback pattern (Node.js style)
const fs = require("fs"); // Node.js example
fs.readFile("config.json", "utf8", (err, data) => {
if (err) {
console.error("Failed to read:", err.message);
return; // MUST return! Otherwise code continues with undefined data.
}
// If we reach here, err is null and data is valid
const config = JSON.parse(data);
console.log("Config loaded:", config);
});
// The repeated error check pattern in callback hell:
step1((err, result1) => {
if (err) return handleError(err); // Check 1
step2(result1, (err, result2) => {
if (err) return handleError(err); // Check 2
step3(result2, (err, result3) => {
if (err) return handleError(err); // Check 3
step4(result3, (err, result4) => {
if (err) return handleError(err); // Check 4
// Finally! 4 error checks for 4 steps.
console.log("All done:", result4);
});
});
});
});
// Compare: With Promises, ONE .catch() handles ALL errors.Inversion of control: You write a callback and give it to a library. The library OWNS when and how it's called. You lose control.
Danger 1: Callback called more than once (library bug) — your code runs twice unexpectedly.
Danger 2: Callback never called (library bug or condition) — your code hangs silently.
Danger 3: Callback called with unexpected arguments — silent corruption.
Danger 4: Callback called synchronously sometimes and asynchronously other times — Zalgo!
Zalgo is a callback that is sometimes sync and sometimes async, creating unpredictable behavior. Code works in testing (sync path) but fails in production (async path).
Zalgo prevention: ALWAYS call callbacks asynchronously — setTimeout(callback, 0) ensures async even if result is ready.
// Inversion of control — you lose control of your callback
function dangerousLibrary(data, callback) {
// Bug: callback called TWICE!
callback(null, data);
// ... later in code ...
callback(null, data); // Your callback runs AGAIN! Double execution!
// Bug: callback never called
if (someCondition) {
callback(null, result); // Only called if condition is true
}
// If condition is false, callback is NEVER called. Silent hang.
}
// Zalgo — sometimes sync, sometimes async
function maybeAsync(cache, key, callback) {
if (cache[key]) {
callback(null, cache[key]); // SYNC! Callback runs immediately!
} else {
fetch(key).then(data => {
callback(null, data); // ASYNC! Callback runs later!
});
}
// Consumer can't predict timing. Breaks assumptions.
}
// Zalgo prevention — ALWAYS async
function alwaysAsync(cache, key, callback) {
if (cache[key]) {
setTimeout(() => callback(null, cache[key]), 0); // Always async!
} else {
fetch(key).then(data => callback(null, data));
}
}
setTimeout(callback, 0). This is called "releasing Zalgo" — it makes behavior predictable.Promises solve ALL callback problems. Here's the complete comparison:
1. Chainable — no pyramid, linear .then() chain. Each step returns a new Promise.
2. Single error channel — one .catch() handles all errors in the chain. No more error checks at every level.
3. Guaranteed async — Promises always resolve/reject asynchronously, no Zalgo possible.
4. Composable — Promise.all, Promise.race, Promise.any for parallel operations.
5. Once settled, never changes — a resolved Promise stays resolved. No double-call bug.
6. Error propagation — errors bubble up the chain automatically, like try/catch.
The callback era is over for new code. But understanding callbacks is essential for: legacy code, Node.js APIs, and understanding what Promises fix.
// Same 4-step flow — Callbacks vs Promises vs Async/Await
// CALLBACKS: Pyramid of doom
getUser(userId, (err, user) => {
if (err) return handleError(err);
getOrders(user.id, (err, orders) => {
if (err) return handleError(err);
getOrderDetails(orders[0].id, (err, details) => {
if (err) return handleError(err);
processPayment(details.total, (err, receipt) => {
if (err) return handleError(err);
console.log("Done:", receipt);
});
});
});
});
// PROMISES: Flat, linear chain
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => processPayment(details.total))
.then(receipt => console.log("Done:", receipt))
.catch(err => handleError(err)); // ONE catch for ALL errors!
// ASYNC/AWAIT: Even cleaner (syntactic sugar over Promises)
async function processOrder(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
const receipt = await processPayment(details.total);
console.log("Done:", receipt);
} catch (err) {
handleError(err); // One try/catch for all steps
}
}Lo kar liya — Key Points:
- ✅ A callback is a function passed as an argument to be called later — sync callbacks run immediately, async callbacks run later
- ✅ Callback hell (pyramid of doom) occurs when each async step depends on the previous result, creating deeply nested code
- ✅ Error-first callback pattern (Node.js convention) uses (err, result) where err is always the first argument
- ✅ Inversion of control means you give your callback to someone else — they decide when/how/if it's called
- ✅ Zalgo is a callback that is sometimes sync and sometimes async — always make callbacks consistently one or the other
- ✅ Promises solve callback problems: linear chaining, single error channel, guaranteed async, composable, no double-call
- ✅ One .catch() handles all errors in a Promise chain — no need for error checks at every level
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login