Async Code in Node.js: Callbacks and Promises
Async Code in Node.js: Callbacks and Promises
Why Async Code Exists in Node.js
Node.js is built on a single-threaded event-driven architecture.
It uses asynchronous programming so that slow operations do not block the entire application.
Examples of slow operations:
Reading files
Database queries
API calls
Network requests
Timers
Without async execution, every request would wait for the previous task to finish.
Synchronous vs Asynchronous Execution
Synchronous Code
const fs = require("fs");
const data = fs.readFileSync("notes.txt", "utf-8");
console.log(data);
console.log("Finished");
Flow
Read file completely ↓ Print file content ↓ Print "Finished"
The program waits until the file reading is completed.
Problem With Blocking Code
Imagine a server handling 10,000 users.
If one file read blocks the thread:
Other users must wait
API becomes slow
Throughput decreases
Node.js avoids this using asynchronous execution.
Callback-Based Async Execution
Basic Callback Example
const fs = require("fs");
console.log("Start");
fs.readFile("notes.txt", "utf-8", (error, data) => {
if (error) {
console.log("Error:", error);
return;
}
console.log(data);
});
console.log("End");
Output
Start End
Step-by-Step Callback Flow
What Happens Internally
Understanding the Callback
Structure
fs.readFile("notes.txt", "utf-8", (error, data) => {
console.log(data);
});
Parameters
Why Callbacks Were Used
Before promises existed:
Callbacks were the standard async mechanism
Node.js APIs were designed around them
Event-driven systems depended heavily on callbacks
Problems With Nested Callbacks
When multiple async operations depend on each other, callbacks become deeply nested.
This is called:
Callback Hell
Pyramid of Doom
Nested Callback Example
const fs = require("fs");
fs.readFile("user.txt", "utf-8", (err, userData) => {
if (err) {
console.log(err);
return;
}
fs.readFile("orders.txt", "utf-8", (err, orderData) => {
if (err) {
console.log(err);
return;
}
fs.readFile("payments.txt", "utf-8", (err, paymentData) => {
if (err) {
console.log(err);
return;
}
console.log(userData);
console.log(orderData);
console.log(paymentData);
});
});
});
Problems in This Code
1. Poor Readability
Code moves toward the right repeatedly.
2. Difficult Error Handling
Each level needs separate error checks.
3. Hard to Maintain
Adding more async operations increases complexity.
4. Difficult Debugging
Tracing execution becomes confusing.
Callback Execution Chain
Promise-Based Async Handling
Promises were introduced to solve callback problems.
A promise represents a future value.
It can be:
Promise Lifecycle Flow
File Reading Using Promises
Using fs/promises
const fs = require("fs/promises");
console.log("Start");
fs.readFile("notes.txt", "utf-8")
.then((data) => {
console.log(data);
})
.catch((error) => {
console.log(error);
});
console.log("End");
Promise Flow
Benefits of Promises
1. Better Readability
Callbacks become flat chains.
2. Centralized Error Handling
.catch((error) => {
console.log(error);
});
One catch block can handle multiple failures.
3. Easier Chaining
fs.readFile("user.txt", "utf-8")
.then((userData) => {
return fs.readFile("orders.txt", "utf-8");
})
.then((orderData) => {
return fs.readFile("payments.txt", "utf-8");
})
.then((paymentData) => {
console.log(paymentData);
})
.catch((error) => {
console.log(error);
});
Callback vs Promise Readability
Callback Version
fs.readFile("a.txt", "utf-8", (err, dataA) => {
fs.readFile("b.txt", "utf-8", (err, dataB) => {
fs.readFile("c.txt", "utf-8", (err, dataC) => {
console.log(dataC);
});
});
});
Promise Version
fs.readFile("a.txt", "utf-8")
.then(() => {
return fs.readFile("b.txt", "utf-8");
})
.then(() => {
return fs.readFile("c.txt", "utf-8");
})
.then((data) => {
console.log(data);
})
.catch((error) => {
console.log(error);
});
Comparison Table
Important Promise Methods
Example With .finally()
fs.readFile("notes.txt", "utf-8")
.then((data) => {
console.log(data);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
console.log("Operation completed");
});
Real-World Use Cases
Promises are heavily used in:
Database drivers
HTTP requests
Authentication systems
Payment gateways
Cloud APIs
Background jobs
Key Takeaways
Callbacks are still present in older Node.js APIs and many legacy applications.
Promises became the modern standard because they:
Improve readability
Simplify async flows
Reduce nesting
Improve error handling
Modern Node.js development heavily uses:
Promises
async/await (built on top of promises)
Understanding callbacks first is important because promises internally solve many callback-related problems.
0 Comments
Sign in to join the conversation
No comments yet. Be the first to comment!