Introduction
A callback is a function passed as an argument to another function, to be executed later once an operation completes. Node.js relies heavily on callbacks because it is single-threaded and event-driven: instead of blocking execution while waiting for I/O (file reads, network requests, database queries), Node.js registers a callback and continues running other code. When the I/O operation finishes, the event loop invokes the callback with the result.
Cricket analogy: A callback is like a substitute fielder told "come on as soon as the injured player leaves the field" — the captain doesn't stop the match waiting, play continues and the substitute is called in only when the moment arrives.
Syntax
const fs = require('fs');
// Asynchronous, non-blocking version
fs.readFile('data.txt', 'utf8', function callback(err, data) {
if (err) {
console.error('Error reading file:', err);
return;
}
console.log('File contents:', data);
});
console.log('Reading file...'); // This runs BEFORE the callbackExplanation
In the example, fs.readFile() is asynchronous: it starts the file-read operation and immediately returns control to the caller, so 'Reading file...' logs first. The callback function is only pushed onto the call stack once the file system operation completes and the event loop picks it up from the callback queue. This pattern lets Node.js handle thousands of concurrent I/O operations on a single thread without waiting idly for any one of them.
Cricket analogy: Like an umpire signaling for a third-umpire review and continuing to manage the over while the replay is processed in the background, fs.readFile() starts the read and returns immediately, letting "Reading file..." log before the callback resolves once the review (I/O) completes.
Example
function fetchUser(id, callback) {
setTimeout(() => {
const user = { id, name: 'Alice' };
callback(null, user);
}, 1000);
}
function fetchOrders(userId, callback) {
setTimeout(() => {
const orders = ['order1', 'order2'];
callback(null, orders);
}, 1000);
}
// Nested callbacks - 'callback hell'
fetchUser(42, (err, user) => {
if (err) return console.error(err);
fetchOrders(user.id, (err, orders) => {
if (err) return console.error(err);
console.log(`${user.name} has orders:`, orders);
});
});Output
The program prints 'Reading file...' style logs immediately, and roughly two seconds later prints 'Alice has orders: [ 'order1', 'order2' ]'. Nesting callbacks like this to sequence dependent async steps is known as 'callback hell' or the 'pyramid of doom' — each new async step adds another indentation level, making code harder to read, debug, and error-handle consistently. This is the primary motivation behind Promises and async/await.
Cricket analogy: Nesting callback after callback to sequence fetchUser then fetchOrders is like stacking review after review in a single delivery — each nested appeal (callback) adds another layer of waiting, which is exactly why cricket streamlined DRS into a clearer single process, just as Promises streamlined async code.
Key Takeaways
- A callback is a function passed to another function to run after an async (or sync) operation finishes.
- Node.js uses callbacks to avoid blocking the single thread during I/O operations.
- Deeply nested callbacks create 'callback hell', hurting readability and error handling.
- Node's convention is the error-first callback signature: (err, result).
- Promises and async/await were introduced largely to solve callback hell.
Practice what you learned
1. Why does Node.js use callbacks for I/O operations?
2. What is 'callback hell'?
3. In the code `fs.readFile('data.txt', 'utf8', function(err, data) {...})`, when does the callback execute?
4. What does the Node.js error-first callback convention require?
Was this page helpful?
You May Also Like
Promises in Node.js
Understanding the Promise object and its pending, fulfilled, and rejected states for cleaner async code.
The Error-First Callback Pattern
The Node.js convention of passing an error as the first callback argument for consistent async error handling.
Node.js Architecture and the Event Loop
Understand Node.js's single-threaded, non-blocking architecture and how the event loop phases process callbacks.
The events Module and EventEmitter
Learn how Node's EventEmitter class enables the observer pattern used throughout the Node.js core APIs.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics