Callbacks
The original async pattern. Pass a function to be called when work is done.
Async Callbacks
A function you pass that gets called when an async operation completes.
// Synchronous callback (runs immediately):
[1, 2, 3].forEach(n => console.log(n));
// Asynchronous callback (runs LATER):
setTimeout(() => {
console.log("I run after 1 second");
}, 1000);The key difference: async callbacks don't run immediately. They're stored and called when the async operation finishes.
Error-First Pattern
Node.js convention: first argument is always the error.
// Error-first callback pattern:
function readFile(path, callback) {
setTimeout(() => {
if (path === "") {
callback(new Error("Path is empty"), null);
} else {
callback(null, "file contents here");
}
}, 100);
}
// Usage — always check error first:
readFile("data.txt", (error, data) => {
if (error) {
console.error("Failed:", error.message);
return; // stop here!
}
console.log("Got:", data);
});Convention:
callback(error, result)
• If error is null → success, use result
• If error exists → failure, handle error
Callback Hell
Nested callbacks become unreadable. The 'Pyramid of Doom'.
// The problem: sequential async operations
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);
getShipping(details.trackingId, (err, shipping) => {
if (err) return handleError(err);
// 😱 4 levels deep! And it gets worse...
displayShipping(shipping);
});
});
});
});Problems with callback hell:
- • Hard to read (grows to the right)
- • Hard to handle errors (repeated error checks)
- • Hard to debug (unclear execution flow)
- • Hard to add/remove steps
- • Variables trapped in scopes
Fixing Callback Hell
Named functions and modularization help — but Promises are the real solution.
// Fix 1: Named functions (flatten the pyramid)
function handleUser(err, user) {
if (err) return handleError(err);
getOrders(user.id, handleOrders);
}
function handleOrders(err, orders) {
if (err) return handleError(err);
getOrderDetails(orders[0].id, handleDetails);
}
function handleDetails(err, details) {
if (err) return handleError(err);
getShipping(details.trackingId, displayShipping);
}
// Start the chain:
getUser(userId, handleUser);// Fix 2: Promises (the modern solution)
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => getShipping(details.trackingId))
.then(shipping => displayShipping(shipping))
.catch(handleError); // one error handler!
// Fix 3: Async/Await (cleanest)
async function showShipping(userId) {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
const shipping = await getShipping(details.trackingId);
displayShipping(shipping);
}Real Examples
Where you still see callbacks today.
// Event listeners (always callbacks):
button.addEventListener("click", () => {
console.log("Clicked!");
});
// Array methods:
[1, 2, 3].map(n => n * 2);
// setTimeout / setInterval:
setTimeout(() => console.log("later"), 1000);
// Node.js fs (legacy API):
const fs = require("fs");
fs.readFile("file.txt", "utf8", (err, data) => {
if (err) throw err;
console.log(data);
});
// Geolocation API:
navigator.geolocation.getCurrentPosition(
(pos) => console.log(pos.coords),
(err) => console.error(err)
);Callbacks aren't dead — event listeners, array methods, and many APIs still use them. But for sequential async operations, Promises and async/await are far better.
FAQ
Common questions about callbacks.