Async Code in Node.js: Callbacks and Promises
When you first start working with Node.js, one concept keeps showing up everywhere: asynchronous code.
At first glance, it feels confusing — why not just run code line by line like normal programs?
Let’s break it down step by step.
Why Async Code Exists in Node.js
Node.js is built on a single-threaded, non-blocking event loop architecture.
This means:
It can handle many operations at once
But it does not create a new thread for each task
Now imagine this scenario:
const data = fs.readFileSync("file.txt");
console.log(data);
console.log("Done");
This is blocking (synchronous):
Node waits for the file to be read
Nothing else happens during that time
Problem
If file reading takes time:
Your server is stuck
Other users have to wait
Solution: Asynchronous Code
fs.readFile("file.txt", "utf-8", (err, data) => {
console.log(data);
});
console.log("Done");
Now:
Node does not wait
It continues execution
Handles the result later
Real Scenario: File Reading
Let’s understand the flow properly.
Code:
console.log("Start");
fs.readFile("file.txt", "utf-8", (err, data) => {
if (err) {
console.log("Error:", err);
return;
}
console.log("File Data:", data);
});
console.log("End");
Step-by-Step Execution
"Start"is printedfs.readFileis sent to the system (async task)Node does NOT wait
"End"is printedOnce file reading completes → callback executes
"File Data"is printed
Output Order
Start
End
File Data: ...
This is the core idea of async in Node.js.
Callback-Based Async Execution
A callback is simply:
A function passed as an argument to another function, executed later.
Example:
function fetchData(callback) {
setTimeout(() => {
callback("Data received");
}, 1000);
}
fetchData((data) => {
console.log(data);
});
Key Idea
You give control to another function
It calls you back when work is done
Problem: Callback Hell
Now let’s chain multiple async operations:
fs.readFile("file1.txt", "utf-8", (err, data1) => {
fs.readFile("file2.txt", "utf-8", (err, data2) => {
fs.readFile("file3.txt", "utf-8", (err, data3) => {
console.log(data1, data2, data3);
});
});
});
Issues
Deep nesting (pyramid shape)
Hard to read
Hard to debug
Error handling becomes messy
This is called:
Callback Hell (or Pyramid of Doom)
Promise-Based Async Handling
Promises were introduced to fix these problems.
What is a Promise?
A Promise represents:
A value that will be available in the future
States of a Promise
Pending
Resolved (fulfilled)
Rejected
Basic Example
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data received");
}, 1000);
});
promise.then((data) => {
console.log(data);
});
Rewriting File Example Using Promises
Using fs.promises:
const fs = require("fs").promises;
fs.readFile("file1.txt", "utf-8")
.then((data1) => {
return fs.readFile("file2.txt", "utf-8")
.then((data2) => {
return fs.readFile("file3.txt", "utf-8")
.then((data3) => {
console.log(data1, data2, data3);
});
});
})
.catch((err) => {
console.log(err);
});
Better Version (Flat Chain)
fs.readFile("file1.txt", "utf-8")
.then((data1) => {
console.log(data1);
return fs.readFile("file2.txt", "utf-8");
})
.then((data2) => {
console.log(data2);
return fs.readFile("file3.txt", "utf-8");
})
.then((data3) => {
console.log(data3);
})
.catch((err) => {
console.log(err);
});
Benefits of Promises
1. Better Readability
No deep nesting
Linear flow
2. Centralised Error Handling
.catch((err) => {
console.log(err);
});
Instead of handling errors at every level.
3. Easier Composition
You can chain multiple async operations cleanly.
4. Foundation for Async/Await
Promises enable:
const data = await fs.readFile("file.txt", "utf-8");
Callback vs Promise Comparison
| Feature | Callbacks | Promises |
|---|---|---|
| Readability | Poor (nested) | Clean (chainable) |
| Error Handling | Scattered | Centralized (.catch) |
| Debugging | Difficult | Easier |
| Scalability | Poor | Better |
| Structure | Pyramid-like | Linear flow |
Final Insight
Callbacks are low-level primitives
Promises are structured abstractions
In modern Node.js:
You should prefer Promises (and async/await) over callbacks