Node.js Callbacks
A callback is a function that is passed as an argument to another function, and is then called (executed) after that other function has finished its work. Callbacks are the original way Node.js handled asynchronous operations — tasks that take time to complete, such as reading a file, making a database query, or fetching data from the internet.
Instead of waiting for a slow task to finish, Node.js starts the task and moves on. When the task is done, the callback function is called with the result. This is what keeps Node.js fast and non-blocking.
Understanding Callbacks with a Simple Analogy
Imagine ordering food at a restaurant. Instead of standing at the counter and waiting until the food is ready, the customer is given a number. The customer goes and sits down (continues doing other things). When the food is ready, the waiter calls the number — this is the callback. The customer then responds and picks up the food.
In code: the "calling the number" is the callback function being invoked.
A Simple Callback Example
function greetUser(name, callback) {
const message = "Hello, " + name + "!";
callback(message);
}
function displayMessage(msg) {
console.log(msg);
}
greetUser("Alice", displayMessage);
// Output: Hello, Alice!
Here, displayMessage is passed as a callback. After greetUser builds the message, it calls the callback with the result.
Anonymous Function as a Callback
Callbacks are often written as anonymous functions (functions without a name) directly inside the call:
function greetUser(name, callback) {
const message = "Hello, " + name + "!";
callback(message);
}
greetUser("Bob", function(msg) {
console.log(msg);
});
// Output: Hello, Bob!
Asynchronous Callback – setTimeout Example
setTimeout() is a built-in function that waits a specified number of milliseconds before calling its callback:
console.log("Task 1: Started");
setTimeout(function() {
console.log("Task 2: Completed after 2 seconds");
}, 2000);
console.log("Task 3: Running while Task 2 waits");
Output:
Task 1: Started
Task 3: Running while Task 2 waits
Task 2: Completed after 2 seconds
This clearly shows the non-blocking nature of Node.js. Task 3 does not wait for Task 2 to finish. The callback is executed only when the timer expires.
Callbacks in the File System Module
The most common real-world use of callbacks in Node.js is with the fs module:
const fs = require('fs');
console.log("Before reading file...");
fs.readFile('notes.txt', 'utf8', function(err, data) {
if (err) {
console.log("Error:", err.message);
return;
}
console.log("File content:", data);
});
console.log("After calling readFile (not waiting)...");
Output (assuming the file exists):
Before reading file...
After calling readFile (not waiting)...
File content: (content of notes.txt)
The file reading happens in the background. The callback fires only after the file is fully read.
The Error-First Callback Convention
Node.js follows a standard pattern called the error-first callback convention. The first parameter of every callback function is reserved for an error. If no error occurred, it is null. If an error occurred, it contains the error details.
function divide(a, b, callback) {
if (b === 0) {
callback(new Error("Cannot divide by zero"), null);
} else {
callback(null, a / b);
}
}
divide(10, 2, function(err, result) {
if (err) {
console.log("Error:", err.message);
} else {
console.log("Result:", result); // Result: 5
}
});
divide(10, 0, function(err, result) {
if (err) {
console.log("Error:", err.message); // Error: Cannot divide by zero
} else {
console.log("Result:", result);
}
});
This pattern is used consistently across Node.js core modules and should always be followed when writing custom asynchronous functions.
Callback Hell – The Problem with Nested Callbacks
When multiple asynchronous operations depend on each other, callbacks become deeply nested. This is informally called callback hell or the "pyramid of doom":
const fs = require('fs');
fs.readFile('step1.txt', 'utf8', function(err1, data1) {
if (err1) return console.log(err1);
fs.readFile('step2.txt', 'utf8', function(err2, data2) {
if (err2) return console.log(err2);
fs.readFile('step3.txt', 'utf8', function(err3, data3) {
if (err3) return console.log(err3);
console.log(data1, data2, data3);
// Multiple levels of nesting — hard to read and maintain
});
});
});
This structure becomes difficult to read, debug, and maintain as the number of operations grows.
Avoiding Callback Hell with Named Functions
One way to flatten the structure is by using named functions instead of anonymous ones:
const fs = require('fs');
function readStep1() {
fs.readFile('step1.txt', 'utf8', readStep2);
}
function readStep2(err, data1) {
if (err) return console.log(err);
fs.readFile('step2.txt', 'utf8', function(err, data2) {
if (err) return console.log(err);
console.log("Step 1:", data1);
console.log("Step 2:", data2);
});
}
readStep1();
This is more readable, but the real solution to callback hell is to use Promises or async/await, which are covered in the next two topics.
setInterval – Repeating Callback
let count = 0;
const intervalId = setInterval(function() {
count++;
console.log("Tick:", count);
if (count === 5) {
clearInterval(intervalId);
console.log("Timer stopped.");
}
}, 1000);
Output (one line per second):
Tick: 1
Tick: 2
Tick: 3
Tick: 4
Tick: 5
Timer stopped.
Key Points
- A callback is a function passed as an argument to another function and called when the task is done.
- Callbacks are the foundation of Node.js's asynchronous, non-blocking behavior.
- Node.js follows the error-first callback pattern — the first argument is always an error (or
null). - Callbacks allow the program to continue running while waiting for slow operations to complete.
- Deeply nested callbacks create "callback hell" — hard to read and maintain code.
- Using named functions helps reduce nesting, but Promises and async/await are the preferred modern solutions.
setTimeout(),setInterval(), andfs.readFile()are all examples of callback-based functions.
