Async Code in Node.js: Callbacks and Promises

Why Async Code Exists in Node.js
Node.js is built to handle many tasks without waiting around for each one to finish. That is the whole reason async code matters.
Imagine your app needs to read a file, query a database, and call an API. These tasks can take time. If Node.js stopped and waited for each one to finish before doing anything else, the whole app would feel slow.
Instead, Node.js starts the task and moves on. When the task is done, Node.js comes back to it and continues. That is async behavior.
For an example
Think about downloading a file on your laptop.
You click the download button.
The file starts downloading in the background.
You do not sit there staring at the progress bar.
You continue browsing, watching videos, or doing other work.
Once the download is complete, you get a notification.
Node.js works in a similar way. It starts tasks like file reading or API calls in the background and keeps running other code. When the task finishes, it notifies your program with the result.
Start With a File Reading Example
Let's say we want to read a file called data.txt.
This is a good example because file reading is not instant. It takes some time, so Node.js handles it asynchronously.
What happens step by step
Your code asks Node.js to read the file.
Node.js sends that task to the system.
Your program keeps running instead of freezing.
When the file is ready, Node.js gives you the result.
Callback-Based Async Execution
Before promises became popular, callbacks were the main way to handle async work in Node.js.
A callback is just a function passed into another function. It gets called later when the async task finishes.
Example
const fs = require('fs');
fs.readFile('data.txt', 'utf8', function(err, data) {
if (err) {
console.log('Error reading file');
return;
}
console.log('File content:', data);
});
console.log('This runs before the file is read');
What this code is doing
fs.readFile()starts reading the file.The function inside it is the callback.
If something goes wrong,
errwill contain the error.If the file is read successfully,
datacontains the content.
The last console.log() runs first because file reading does not block the program.
Problems With Nested Callbacks
Callbacks are useful, but they can become messy when one async task depends on another async task.
This is where nested callbacks start to look hard to read.
Example of nested callbacks
const fs = require('fs');
fs.readFile('user.txt', 'utf8', function(err, userData) {
if (err) {
console.log('Could not read user file');
return;
}
fs.readFile('profile.txt', 'utf8', function(err, profileData) {
if (err) {
console.log('Could not read profile file');
return;
}
fs.readFile('settings.txt', 'utf8', function(err, settingsData) {
if (err) {
console.log('Could not read settings file');
return;
}
console.log(userData, profileData, settingsData);
});
});
});
This becomes a problem
This structure is harder to follow because:
indentation keeps increasing
each step is wrapped inside another function
error handling repeats again and again
the code starts to look like a staircase
This is often called callback hell.
Callback nesting diagram
The logic is still correct, but the readability suffers.
Promise-Based Async Handling
Promises were introduced to make async code easier to manage.
A promise is an object that represents a future result.
It can be in one of three states:
pending, which means the task is still running
fulfilled, which means the task completed successfully
rejected, which means the task failed
Promise example
const fs = require('fs').promises;
fs.readFile('data.txt', 'utf8')
.then(function(data) {
console.log('File content:', data);
})
.catch(function(err) {
console.log('Error reading file');
});
console.log('This runs before the file is read');
What is happening here
fs.promises.readFile()returns a promise..then()runs when the file is read successfully..catch()runs if something goes wrong.The main program still keeps moving while the file is being read.
Promise Lifecycle Flow
Why Promises Are Better Than Nested Callbacks
Promises improve readability because they let you write async steps in a cleaner chain.
Callback style
step1(function(err, result1) {
if (err) return;
step2(result1, function(err, result2) {
if (err) return;
step3(result2, function(err, result3) {
if (err) return;
console.log(result3);
});
});
});
Promise style
step1()
.then(function(result1) {
return step2(result1);
})
.then(function(result2) {
return step3(result2);
})
.then(function(result3) {
console.log(result3);
})
.catch(function(err) {
console.log('Something failed');
});
Why the promise version is easier
each step is visible in one chain
errors can be handled in one place with
.catch()the code flows top to bottom
it is easier to maintain
Callback vs Promise
| Criteria | Callbacks | Promises |
|---|---|---|
| Best Use Case | Simple async tasks | Complex async workflows |
| Code Length | Short and minimal | Slightly longer but structured |
| Number of Steps | Works well for 1 or 2 steps | Ideal for multiple steps |
| Readability | Can become messy with nesting | Clean and linear flow |
| Error Handling | Repeated in each callback | Centralized using .catch() |
| Maintainability | Harder as code grows | Easier to manage and scale |
| Code Structure | Nested (callback hell risk) | Chained and organized |
Easy Way to Remember the Difference
A callback says:
"Here is a function. Run it later when you are done."
A promise says:
"Here is a future result. I will tell you when it succeeds or fails."
Conclusion
Async behavior is the backbone of Node.js performance. It allows your application to handle multiple operations without blocking execution, which is critical for building scalable systems.
Callbacks introduced the idea of handling results later, but they become difficult to manage as complexity grows. Promises improve this by organizing async logic into a predictable and readable flow, making both development and debugging easier.
If you understand callbacks, you understand the foundation. If you use promises, you write cleaner and more maintainable code.




