1. Introduction
JavaScript is single-threaded, meaning it can only execute one piece of code at a time on its call stack. Yet browsers and Node.js let JavaScript wait for things like timers, network requests, and file reads without freezing the whole program. This is possible because slow operations are handed off to the runtime (the browser or Node), and JavaScript keeps running other code in the meantime. A callback is simply a function passed as an argument to another function, to be invoked later when that operation finishes. Callbacks were the original way JavaScript handled asynchronous work, long before Promises or async/await existed.
Cricket analogy: A single umpire can only watch one thing at a time, but by delegating boundary-line decisions to assistant umpires, the runtime, the main umpire keeps the match moving instead of freezing play; the assistants call back with a decision once ready, the way scoring was done before third-umpire tech existed.
Under the hood, the JavaScript engine relies on an event loop. The event loop continuously checks whether the call stack is empty; when it is, it pulls the next task from a queue (the callback/task queue) and pushes it onto the stack to run. This is what allows setTimeout, DOM events, and I/O callbacks to run 'later' without blocking the main thread.
Cricket analogy: The match referee only calls in the next scheduled fixture once the current match on the ground has completely finished, stack empty, pulling the next game from the tournament's queued schedule, the task queue, exactly when the ground clears.
2. Syntax
// A function that accepts a callback
function doSomethingAsync(input, callback) {
setTimeout(() => {
// simulate work, then call the callback with the result
callback(null, input * 2);
}, 100);
}
doSomethingAsync(21, (error, result) => {
if (error) {
console.error(error);
return;
}
console.log(result); // 42
});3. Explanation
When you call an asynchronous function like setTimeout, JavaScript does not wait. It registers the callback with the runtime and immediately continues executing the rest of the synchronous code. Only once the call stack is completely empty does the event loop take the next callback from the task (macrotask) queue and run it. This is why 'later' callbacks always run after all currently queued synchronous code has finished, even if the delay is 0ms.
Cricket analogy: Scheduling a post-match press conference doesn't pause the celebration on the field; the team keeps celebrating, sync code, and only once every player has left the ground, stack empty, does the press conference, the callback, actually begin, even if it was scheduled immediately.
A common Node.js convention is the 'error-first callback': the first argument is reserved for an error (or null if none occurred), and the second is the result. This pattern works, but it does not compose well. When you need to perform several async steps in sequence -- fetch a user, then their orders, then order details -- each step must be nested inside the previous callback. The result is deeply indented code known as 'callback hell' or the 'pyramid of doom', where error handling has to be duplicated at every level and the logical flow becomes hard to follow.
Cricket analogy: Checking for rain, the error, before confirming the toss result, then checking ground fitness before confirming innings start, then checking bad light before confirming close of play, nests three separate check-then-proceed steps into a deeply indented sequence of if-no-problem-then reports for the scorer to follow.
Callback hell is not just an aesthetic problem. Each nested callback needs its own error check, it is easy to forget one, and a thrown synchronous error inside an async callback will NOT be caught by a surrounding try/catch -- try/catch only guards code that runs synchronously within it. This is one of the main reasons Promises (and later async/await) were introduced: they let asynchronous errors propagate through a single, predictable channel.
4. Example
function getUser(id, callback) {
setTimeout(() => {
console.log("Fetched user");
callback({ id, name: "Alice" });
}, 100);
}
function getOrders(user, callback) {
setTimeout(() => {
console.log("Fetched orders for " + user.name);
callback(["order1", "order2"]);
}, 100);
}
function getOrderDetails(order, callback) {
setTimeout(() => {
console.log("Fetched details for " + order);
callback({ order, total: 42 });
}, 100);
}
console.log("Start");
getUser(1, (user) => {
getOrders(user, (orders) => {
getOrderDetails(orders[0], (details) => {
console.log("Details:", details);
console.log("Done");
});
});
});
console.log("End");5. Output
Start
End
Fetched user
Fetched orders for Alice
Fetched details for order1
Details: { order: 'order1', total: 42 }
Done6. Key Takeaways
- JavaScript is single-threaded; async operations are handed off to the runtime and their callbacks run later via the event loop.
- A callback is a function passed to another function to be invoked once an operation completes.
- Synchronous code (including console.log calls before/after the async call) always runs before any queued callback, even with a 0ms delay.
- Nesting callbacks to sequence multiple async steps leads to 'callback hell' / the 'pyramid of doom'.
- try/catch cannot catch errors thrown inside an asynchronous callback, since it has already exited the try block by the time the callback runs.
- Error-first callbacks (error, result) are the classic Node.js convention, but they don't compose as cleanly as Promises.
Practice what you learned
1. What is a 'callback' in JavaScript?
2. What will the following code print first: `console.log('A'); setTimeout(() => console.log('B'), 0); console.log('C');`?
3. What is 'callback hell'?
4. Why can't a try/catch around an async call catch an error thrown later inside its callback?
5. Which convention is commonly used for Node.js-style callbacks?
Was this page helpful?
You May Also Like
Promises in JavaScript
Understand the Promise object, its three states, and how .then/.catch/.finally chaining replaces nested callbacks.
async/await in JavaScript
Learn how async/await provides synchronous-looking syntax for working with Promises, including error handling with try/catch.
The Event Loop in JavaScript
Understand the call stack, macrotask queue, and microtask queue, and master the ordering rules that govern async execution.
setTimeout and setInterval in JavaScript
Schedule delayed or repeating code with setTimeout and setInterval, and learn about timer drift and the need to clear intervals.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics