Callbacks
A callback is a function passed as an argument to another function, to be called when an async operation completes. This is the oldest pattern for handling async results in JavaScript.
function fetchUser(id, callback) {
setTimeout(() => {
callback({ id, name: "Alice" });
}, 1000);
}
fetchUser(1, (user) => {
console.log(user.name); // "Alice" (after ~1s)
});
The fetchUser function simulates an async operation with setTimeout. When the data is “ready,” it calls the provided callback with the result. The caller does not block. It passes a function that says what to do when the result arrives.
Error-First Callbacks
In Node.js, callbacks follow the error-first convention: the first argument to the callback is an error (or null if no error), and subsequent arguments carry the result.
import fs from "fs";
fs.readFile("data.json", "utf8", (error, data) => {
if (error) {
console.error("Failed to read file:", error.message);
return;
}
console.log(data);
});
This pattern ensures you always check for errors before processing the result:
function getUser(id, callback) {
setTimeout(() => {
if (id <= 0) {
callback(new Error("Invalid user ID"));
return;
}
callback(null, { id, name: "Alice" });
}, 1000);
}
getUser(1, (error, user) => {
if (error) {
console.error(error.message);
return;
}
console.log(user.name); // "Alice"
});
Callback Hell
The problem is that when async operations depend on each other, callbacks nest inside callbacks. This creates deeply indented code known as callback hell (or the 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);
getShippingStatus(details.trackingId, (err, status) => {
if (err) return handleError(err);
console.log("Shipping status:", status);
});
});
});
});
Problems with callback hell:
- Hard to read: The logic flows rightward instead of downward
- Error handling is repetitive: Every level needs its own error check
- Difficult to compose: Combining parallel or conditional async work is awkward
- Breaks control flow: You cannot use
return,throw, ortry/catchacross callback boundaries
You can reduce callback hell by pulling each level out into a named function, but the limitations above are still there. My advice is to avoid deeply nested callbacks whenever possible. Promises come next, and they fix these problems. Promises give you an API you can chain and compose.