Async Code in Node.js: Callbacks and Promises
Flow of how Async code evolved from Callbacks to Promises.

If you’ve been working with JavaScript, especially Node.js, you must have noticed that a lot of things are asynchronous by default. Reading files, making API calls, database operations, all of these do not block execution. This is not accidental, this is how Node.js is designed to work efficiently. Since JavaScript runs on a single thread, Node.js relies heavily on asynchronous behavior so that one slow operation does not stop everything else from running.
So the reason async code exists in Node.js is simple, to avoid blocking. If every file read or database call was synchronous, the server would get stuck handling one request at a time, which would make it slow and inefficient. Async behavior allows Node.js to handle multiple operations without waiting for each one to finish before moving ahead.
Starting with a file reading example
Let’s take a very simple example of reading a file. In Node.js, we usually use asynchronous file reading so that the program does not block.
const fs = require("fs");
console.log("Start");
fs.readFile("data.txt", "utf-8", (err, data) => {
if (err) {
console.log("Error reading file");
return;
}
console.log("File content:", data);
});
console.log("End");
Here, even though the file is being read in between, the output will be:
**Start
**End
*File content: ...
This happens because file reading is asynchronous. Node.js starts the operation and moves ahead, and once the data is ready, the callback function is executed.
Callback based async execution
Callbacks are one of the earliest ways to handle asynchronous code in JavaScript. A callback is simply a function that is passed as an argument and is executed later when the async task is completed.
In the previous example, the function inside readFile is the callback. It runs only after the file has been read.
The flow is simple, start the task, continue execution, and once the task finishes, run the callback. This allows non blocking behavior.
Problem with nested callbacks
The problem starts when we have multiple async operations that depend on each other. Then callbacks start getting nested inside each other, which makes the code harder to read and maintain.
const fs = require("fs");
fs.readFile("file1.txt", "utf-8", (err, data1) => {
if (err) return console.log(err);
fs.readFile("file2.txt", "utf-8", (err, data2) => {
if (err) return console.log(err);
fs.readFile("file3.txt", "utf-8", (err, data3) => {
if (err) return console.log(err);
console.log(data1, data2, data3);
});
});
});
This structure keeps going deeper as more operations are added. It becomes difficult to follow the flow, handle errors properly, and debug issues. This is commonly referred to as callback nesting, and it quickly becomes messy.
Promise based async handling
To solve the readability issue of callbacks, promises were introduced. A promise represents a value that may be available now, later, or never. Instead of passing callbacks, we return promises and handle results using then and catch.
Let’s convert the file reading example into promise based code.
const fs = require("fs").promises;
console.log("Start");
fs.readFile("data.txt", "utf-8")
.then((data) => {
console.log("File content:", data);
})
.catch((err) => {
console.log("Error reading file");
});
console.log("End");
The behavior is still asynchronous, but the structure is cleaner. Instead of nesting, we chain operations.
Handling multiple operations with promises
The earlier nested example can now be written in a much cleaner way.
const fs = require("fs").promises;
fs.readFile("file1.txt", "utf-8")
.then((data1) => {
return fs.readFile("file2.txt", "utf-8").then((data2) => {
return { data1, data2 };
});
})
.then(({ data1, data2 }) => {
return fs.readFile("file3.txt", "utf-8").then((data3) => {
console.log(data1, data2, data3);
});
})
.catch((err) => {
console.log(err);
});
Even though this is still not perfect, it is more readable compared to deeply nested callbacks.
Benefits of promises
Promises solve multiple issues that callbacks had. The code becomes flatter, easier to read, and error handling becomes more consistent because we can use a single catch block. It also becomes easier to chain operations and manage flow without going deep into nested structures.
Another advantage is that promises can be combined with async and await, which makes the code even more readable and closer to synchronous style, while still being asynchronous underneath.
Final clarity
Async code exists in Node.js to prevent blocking and to make efficient use of the single threaded nature of JavaScript. Callbacks were the initial way to handle async operations, but they become difficult to manage when multiple steps are involved. Promises improve this by providing a cleaner and more structured way to handle async flow.
So the progression becomes clear, callbacks handle async tasks but can get messy, promises improve readability and structure, and later async and await make it even simpler to write and understand.



