Async Code in Node.js: Callbacks and Promises

Software Engineer | Passionate about Web Development, DSA & Problem Solving. I write simple, practical tech blogs to help developers learn and grow. Exploring JavaScript, C++, Backend & Modern Web Technologies.
Introduction
Node.js is designed to handle multiple operations efficiently using a non-blocking, asynchronous architecture. Unlike traditional systems where tasks execute one after another, Node.js allows tasks like file reading, database queries, and API calls to run in the background.
But this raises an important question:
How does Node.js manage operations that take time without stopping execution?
The answer lies in asynchronous programming, primarily using:
Callbacks (traditional approach)
Promises (modern approach)
Understanding these concepts is crucial because they form the foundation of how real-world Node.js applications work.
1. Why Async Code Exists in Node.js
Node.js runs on a single thread, meaning it can execute only one task at a time.
If it used only synchronous (blocking) code:
One slow operation would block the entire server
All users would experience delays
Real-World Scenario (File Reading)
Imagine a server reading a large file:
If blocking → server waits
If non-blocking → server continues handling other users
Example (Blocking):
const fs = require("fs");
const data = fs.readFileSync("file.txt", "utf8");
console.log(data);
Explanation:
Execution stops until file is fully read
No other task can run
Example (Async):
const fs = require("fs");
fs.readFile("file.txt", "utf8", (err, data) => {
console.log(data);
});
Explanation:
File reading happens in background
Server continues executing other code
Callback runs when file is ready
2. Callback-Based Async Execution
A callback is a function passed as an argument to another function and executed later.
Example:
function fetchData(callback) {
setTimeout(() => {
callback("Data received");
}, 1000);
}
fetchData((result) => {
console.log(result);
});
Explanation :
fetchDatais calledsetTimeoutsimulates async taskCallback is stored
After 1 second → callback executes
"Data received"is printed
Output:
Data received
Key Idea:
Callback runs after async task completes
Allows non-blocking execution
3. Problems with Nested Callbacks (Callback Hell)
Callbacks work well initially, but become problematic when multiple async tasks depend on each other.
Example (Nested Callbacks):
setTimeout(() => {
console.log("Step 1");
setTimeout(() => {
console.log("Step 2");
setTimeout(() => {
console.log("Step 3");
}, 1000);
}, 1000);
}, 1000);
Explanation:
Each step depends on previous one
Code becomes deeply nested
Hard to read and maintain
Output:
Step 1
Step 2
Step 3
Problems:
Poor readability
Difficult debugging
Error handling becomes complex
This situation is called callback hell.
4. Promise-Based Async Handling
Promises were introduced to solve callback problems.
A Promise represents a future value.
Example:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data received");
}, 1000);
});
promise.then(result => console.log(result));
Explanation :
Promise starts in pending state
After 1 second →
resolve()is called.then()executes with result
Output:
Data received
Key Idea:
Cleaner structure than callbacks
Separates success and failure
5. Promise Chaining (Replacing Nested Callbacks)
Promises allow chaining multiple async operations.
Example:
function step1() {
return Promise.resolve("Step 1");
}
function step2() {
return Promise.resolve("Step 2");
}
function step3() {
return Promise.resolve("Step 3");
}
step1()
.then(result => {
console.log(result);
return step2();
})
.then(result => {
console.log(result);
return step3();
})
.then(result => {
console.log(result);
});
Explanation:
Each
.then()waits for previous stepNo nesting required
Code flows linearly
Output:
Step 1
Step 2
Step 3
6. Benefits of Promises
Promises improve asynchronous programming in multiple ways.
Better Readability
Linear flow instead of nested structure
Easy Error Handling
promise
.then(result => console.log(result))
.catch(err => console.log(err));
Chaining Support
Multiple async tasks handled sequentially
Foundation for Async/Await
Promises are the base of modern async syntax
7. Callback vs Promise Readability
Callback Style:
doTask1(() => {
doTask2(() => {
doTask3(() => {
console.log("Done");
});
});
});
Promise Style:
doTask1()
.then(doTask2)
.then(doTask3)
.then(() => console.log("Done"));
Explanation:
Callback → nested and complex
Promise → flat and readable
Conclusion
Asynchronous programming is the backbone of Node.js.
Callbacks introduced async handling but led to complexity
Promises improved structure, readability, and error handling
By understanding both:
You can maintain legacy code (callbacks)
Write modern scalable applications (promises)




