AiTechWorlds
AiTechWorlds
University learning companion
JavaScript Complete 2026 · Callbacks & Callback Hell
Use this course-level map to place “Callbacks & Callback Hell” in the larger system. It complements—not replaces—a topic-specific worked diagram.
Callbacks were JavaScript's original solution to asynchronous programming. Understanding them is essential both because they're still used everywhere, and because their problems motivated Promises and async/await.
A callback is simply a function passed to another function to be called later:
// Synchronous callback — called immediately
[1, 2, 3].forEach(function(n) {
console.log(n); // callback called for each element
});
// Asynchronous callback — called after a delay or event
setTimeout(function() {
console.log("After 1 second");
}, 1000);
// addEventListener takes a callback
button.addEventListener("click", function(e) {
console.log("Clicked!");
});Node.js established a convention where async callbacks always receive (error, result):
const fs = require("fs");
// Node.js fs callbacks: (err, result)
fs.readFile("data.txt", "utf8", function(err, contents) {
if (err) {
console.error("Failed to read:", err.message);
return;
}
console.log(contents);
});
// The pattern: always handle error first
function loadUser(id, callback) {
database.query("SELECT * FROM users WHERE id = ?", [id], function(err, rows) {
if (err) {
callback(err, null);
return;
}
callback(null, rows[0]);
});
}
loadUser(42, function(err, user) {
if (err) {
handleError(err);
return;
}
renderUser(user);
});When you need to chain multiple async operations, callbacks nest deeper and deeper:
// Real-world scenario: login → load profile → load permissions → load dashboard
loginUser(username, password, function(err, user) {
if (err) { handleError(err); return; }
loadProfile(user.id, function(err, profile) {
if (err) { handleError(err); return; }
loadPermissions(user.id, function(err, permissions) {
if (err) { handleError(err); return; }
loadDashboard(user, profile, permissions, function(err, dashboard) {
if (err) { handleError(err); return; }
render(dashboard);
// Nested 4 levels deep — and this is a simple example
});
});
});
});This is "callback hell" (also called the "pyramid of doom"). Problems:
function onLogin(err, user) {
if (err) { handleError(err); return; }
loadProfile(user.id, onProfileLoaded.bind(null, user));
}
function onProfileLoaded(user, err, profile) {
if (err) { handleError(err); return; }
loadPermissions(user.id, onPermissionsLoaded.bind(null, user, profile));
}
function onPermissionsLoaded(user, profile, err, permissions) {
if (err) { handleError(err); return; }
render({ user, profile, permissions });
}
loginUser(username, password, onLogin);// async.js waterfall — sequential async tasks
async.waterfall([
(cb) => loginUser(username, password, cb),
(user, cb) => loadProfile(user.id, (err, profile) => cb(err, user, profile)),
(user, profile, cb) => loadPermissions(user.id, (err, perms) => cb(err, user, profile, perms)),
], function(err, user, profile, permissions) {
if (err) { handleError(err); return; }
render({ user, profile, permissions });
});These patterns work but are complex. Promises were introduced to fix this at the language level.
// Callback version
function getUser(id, callback) {
fetch(`/api/users/${id}`)
.then(r => r.json())
.then(data => callback(null, data))
.catch(err => callback(err, null));
}
// Promise version — what you should write today
function getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json());
}
// async/await version — even cleaner
async function getUser(id) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}Convert callback based APIs to Promises:
// Manual promisify
function readFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, "utf8", (err, contents) => {
if (err) reject(err);
else resolve(contents);
});
});
}
// Node.js util.promisify — automatic
const { promisify } = require("util");
const readFileAsync = promisify(fs.readFile);
const contents = await readFileAsync("data.txt", "utf8");
// Node.js fs/promises — already promisified
const { readFile } = require("fs/promises");
const data = await readFile("data.txt", "utf8");Not everything needs to be a Promise:
// Event handlers — callbacks are the right tool
button.addEventListener("click", handleClick);
// Array methods — synchronous callbacks are fine
const doubled = numbers.map(n => n * 2);
// Simple one-time callbacks in your own APIs
function processItems(items, onItem, onDone) {
items.forEach(onItem);
onDone();
}
// Streaming (ongoing, not one-time)
stream.on("data", (chunk) => process(chunk));
stream.on("end", () => cleanup());For anything involving async operations (network, file I/O, timers), use Promises and async/await. Callbacks are the foundation but Promises are the standard today.
Next lesson: Promises creating and consuming them for clean async code.
University learning companion
JavaScript Complete 2026 · Callbacks & Callback Hell
Complete the steps without looking at the answer first; record evidence rather than only a final result.
These are primary or established learning references for this subject. Check the relevant chapter or specification for the exact topic and version.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises