Callbacks
A callback is a function you hand to another function so it can call it back later. Callbacks come in two flavours that look identical but behave very differently: synchronous callbacks (called immediately during execution) and asynchronous callbacks (called later, after some external event). Understanding which kind you are using is essential for reasoning about order.
Synchronous callbacks
A synchronous callback is invoked while the function you handed it to is running. The classic examples are array methods.
const nums = [1, 2, 3];
console.log("before");
nums.forEach(n => console.log(" visit", n));
console.log("after");before visit 1 visit 2 visit 3 after
The callback runs immediately, three times, before forEach returns. Nothing is queued for later — it is just a way to pass behaviour into another function.
Asynchronous callbacks
An asynchronous callback is handed to an API that says "I'll call this later, when something happens" — a timer firing, a network response arriving, a user clicking. Your function returns immediately; the callback runs at some future point on the event loop.
console.log("before");
setTimeout(() => {
console.log(" delayed");
}, 1000);
console.log("after");before after delayed (~1 second later)
setTimeout registers the callback with the host environment and returns. The "after" log happens next. The browser fires the timer ~1s later and queues the callback for the event loop.
Common asynchronous callback APIs
Timers —
setTimeout,setInterval,requestAnimationFrame.DOM events —
element.addEventListener("click", handler).Network (older APIs) —
XMLHttpRequest, Node's file systemfs.readFile(path, cb).Node patterns — almost everything in classic Node took a callback
(err, result) => ....
Classic Node error-first pattern
// fs.readFile("config.json", "utf8", (err, data) => {
// if (err) return console.error(err);
// console.log("got config:", data);
// });Why order is sometimes surprising
The single most common bug for beginners is treating asynchronous callbacks as if they were synchronous:
function loadUser(id) {
let result;
setTimeout(() => { result = { id, name: "Ada" }; }, 100);
return result; // returns undefined — the timeout hasn't fired yet!
}
console.log(loadUser(1)); // undefinedAnything that depends on the async result must happen inside the callback (or use a Promise / async/await):
function loadUser(id, cb) {
setTimeout(() => cb({ id, name: "Ada" }), 100);
}
loadUser(1, user => {
console.log(user); // runs ~100ms later
});Callback hell — what we used to live with
Before Promises and async/await, chaining several asynchronous steps led to deeply nested code — the famous "pyramid of doom":
getUser(id, (err, user) => {
if (err) return done(err);
getOrders(user.id, (err, orders) => {
if (err) return done(err);
getInvoice(orders[0].id, (err, invoice) => {
if (err) return done(err);
sendEmail(user.email, invoice, (err) => {
if (err) return done(err);
done(null, "all good");
});
});
});
});Each level adds another indentation, another if (err) block, and another place where you can forget to handle an error. Promises and async/await flatten this dramatically — we cover them in Promises and async/await.
Writing your own callback-style API
Sometimes you still write callbacks — most often for synchronous "configure this behaviour" helpers.
function repeat(times, callback) {
for (let i = 0; i < times; i++) callback(i);
}
repeat(3, i => console.log("step", i));step 0 step 1 step 2