Async Code in Node.js: Callbacks and Promises

When building applications in Node.js, many operations take time to complete.
Examples:
Reading files
Fetching data from APIs
Connecting to databases
Waiting for user input
If Node.js waited for each task to finish before moving forward, applications would become slow and inefficient.
This is why Node.js uses asynchronous code.
In this article, we’ll understand:
Why async code exists in Node.js
Callback-based asynchronous execution
Problems with nested callbacks
Promise-based async handling
Benefits of promises
Let’s begin with a real-world example.
Why Does Async Code Exist in Node.js?
Imagine reading a large file from your computer.
If Node.js stopped everything until the file finished loading, the entire application would freeze.
Instead, Node.js starts the file-reading task and continues doing other work.
When the file is ready, Node.js handles the result later.
This is called asynchronous programming.
Synchronous vs Asynchronous Code
Synchronous Code
console.log("Start");
console.log("Reading file...");
console.log("End");
Output:
Start
Reading file...
End
The code runs line by line in order.
Asynchronous Example
Node.js provides asynchronous functions like fs.readFile().
Example:
const fs = require("fs");
console.log("Start");
fs.readFile("demo.txt", "utf8", (err, data) => {
console.log(data);
});
console.log("End");
Possible output:
Start
End
File content here
Notice:
Node.js does not wait for the file to finish reading
It continues executing the next lines
The callback runs later when the file is ready
What is a Callback?
A callback is a function passed into another function to run later.
Example:
function greet(name, callback) {
console.log("Hello " + name);
callback();
}
function sayBye() {
console.log("Goodbye");
}
greet("Abhi", sayBye);
Output:
Hello Abhi
Goodbye
Here:
sayByeis passed as a callbackIt executes after the greeting
Callback-Based Async Execution
Let’s understand async flow step by step.
Example:
const fs = require("fs");
console.log("1. Start Reading");
fs.readFile("demo.txt", "utf8", (err, data) => {
console.log("2. File Content:", data);
});
console.log("3. Continue Execution");
Output:
1. Start Reading
3. Continue Execution
2. File Content: Hello World
How the Callback Flow Works
Start Program
↓
Start File Reading
↓
Continue Other Code
↓
File Reading Completes
↓
Callback Function Executes
This is the foundation of asynchronous programming in Node.js.
Problems with Nested Callbacks
Callbacks work well initially, but deeply nested callbacks can make code difficult to read.
Example:
loginUser(function(user) {
getPosts(user, function(posts) {
getComments(posts, function(comments) {
console.log(comments);
});
});
});
This structure becomes hard to manage.
This problem is often called:
Callback Hell
Problems include:
Hard to read
Difficult to debug
Deep nesting
Poor maintainability
Introduction to Promises
Promises were introduced to make asynchronous code cleaner and easier to manage.
A Promise represents a value that may be available:
Now
Later
Or never
Creating a Promise
Example:
const promise = new Promise((resolve, reject) => {
let success = true;
if (success) {
resolve("Operation Successful");
} else {
reject("Operation Failed");
}
});
A promise has two possible outcomes:
resolve()→ successreject()→ failure
Handling Promises
We use:
.then()for success.catch()for errors
Example:
promise
.then((message) => {
console.log(message);
})
.catch((error) => {
console.log(error);
});
Output:
Operation Successful
Promise-Based Async Handling
Let’s rewrite a file-reading example using promises.
Example:
const fs = require("fs").promises;
fs.readFile("demo.txt", "utf8")
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log(err);
});
This looks cleaner compared to nested callbacks.
Callback vs Promise Readability
Callback Style
getUser(function(user) {
getOrders(user, function(orders) {
console.log(orders);
});
});
Promise Style
getUser()
.then((user) => getOrders(user))
.then((orders) => console.log(orders))
.catch((err) => console.log(err));
Promises reduce nesting and improve readability.
Benefits of Promises
Promises provide several advantages:
✅ Cleaner code
✅ Better readability
✅ Easier error handling
✅ Avoid callback hell
✅ Better async chaining
This is why modern Node.js applications heavily use promises.
Promise Lifecycle
A promise has three states.
Pending
↓
Resolved OR Rejected
States
| State | Meaning |
|---|---|
| Pending | Operation still running |
| Resolved | Operation successful |
| Rejected | Operation failed |
Practice Assignment
Try these tasks yourself.
1. Callback Example
Create a function that waits 2 seconds and then prints a message using a callback.
Example:
function fetchData(callback) {
setTimeout(() => {
callback("Data received");
}, 2000);
}
2. Promise Example
Create a promise that resolves after 2 seconds.
Example:
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve("Data loaded");
}, 2000);
});
3. Handle the Promise
Use .then() to print the resolved value.
And now, you know what Callbacks and Promises in Node.js are.
If you have any doubt or want to connect, feel free to drop a comment — I’d be happy to help.
Thanks for reading, and see you in the next blog!
Peace ✌️ and Happy Learning!




