Async Code in Node.js: Callbacks and Promises

Learning web development in public. Writing simple, real-world explanations about web development concepts. Helping beginners understand why things work, not just how.
Search for a command to run...

Learning web development in public. Writing simple, real-world explanations about web development concepts. Helping beginners understand why things work, not just how.
No comments yet. Be the first to comment.
Every day, millions of users upload photos, videos, stories, and reels to Instagram. From a user's perspective, the process appears simple: select media, apply filters, add a caption, and tap "Post."
Building Offline-First Messaging Apps: How Messages Work Without Internet Modern messaging applications have transformed the way people communicate. Whether it's chatting with friends, collaborating w
In this article we'll explore about the Expo Router and React Navigation and answer which one to use in 2026. If you build mobile apps using React Native, one thing becomes obvious very quickly: Navig

Modern mobile apps are no longer just a collection of screens connected together. Apps like Instagram, WhatsApp, Uber, and Netflix operate at massive scale with millions of users, real time systems, o
In this article we'll be exploring react.js and the things of react.js that makes it popular and stand out among other libraries ( no fight over library vs framework ). We'll go through: What problem

Shkaai
68 posts
In this article, we’ll understand one of the most important concepts in Node.js — asynchronous code.
Why async code exists in Node.js
Callback-based async execution
Problems with nested callbacks
Promise-based async handling
Benefits of promises
Let’s start with a basic question:
Why doesn’t Node.js execute everything line by line like traditional programs?
Because Node.js is single-threaded.
This means:
It can run only one task at a time
But it still needs to handle multiple users, requests, file operations, and API calls
Now imagine this:
const data = readFileSync("largeFile.txt");
console.log(data);
If the file is large, Node.js will:
Stop execution
Wait until the file is fully read
Then move forward
This creates a problem:
While waiting, the server cannot handle other requests.
Instead of waiting, Node.js uses async code.
The idea is simple:
Start a task, and when it finishes, notify me. Meanwhile, I will continue doing other work.
Example:
readFile("file.txt", (err,data) => {
console.log(data);
});
console.log("This runs first");
Output:
This runs first
(file content later)
Node.js does not block execution, which makes it fast and efficient.
The first approach to handling async operations in Node.js was callbacks.
A callback is simply a function passed as an argument to another function, which is executed later.
Example:
function fetchData(callback) {
setTimeout(() => {
callback("Data received");
},2000);
}
fetchData((data) => {
console.log(data);
});
Flow:
The async task starts
Node.js continues execution
Once the task finishes, the callback runs
Now consider multiple dependent async operations:
loginUser((user) => {
getProfile(user, (profile) => {
getPosts(profile, (posts) => {
console.log(posts);
});
});
});
This structure is called callback hell.
Problems:
Code becomes deeply nested
Hard to read and understand
Debugging becomes difficult
Error handling is inconsistent
This pattern is often called the "pyramid of doom".
To solve the problems of callbacks, JavaScript introduced promises.
A promise represents a value that will be available in the future.
It has three states:
Pending
Resolved
Rejected
function fetchData() {
return new Promise((resolve,reject) => {
setTimeout(() => {
resolve("Data received");
},2000);
});
}
fetchData()
.then((data) => {
console.log(data);
})
.catch((err) => {
console.error(err);
});
loginUser()
.then((user) =>getProfile(user))
.then((profile) =>getPosts(profile))
.then((posts) =>console.log(posts))
.catch((err) =>console.error(err));
This removes nesting and creates a clear, linear flow.
Code is flatter and easier to understand compared to nested callbacks.
Instead of handling errors at every step, a single .catch() can handle failures.
Multiple async steps can be connected in a clean sequence.
Promises enable modern syntax like async/await:
async function run() {
const data = await fetchData();
console.log(data);
}
This looks like synchronous code but works asynchronously.
Node.js uses async code to avoid blocking execution
Callbacks were the first solution but led to complex and unreadable code
Promises improved structure and made async code easier to manage
Today, async/await is the most commonly used approach, built on top of promises