Async Code in Node.js: Callbacks and Promises
I build clean and simple web experiences and learn something new every day.
The first time I started reading Node.js code… I kept seeing things execute in a weird order.
Like this:
console.log("Start");
setTimeout(() => {
console.log("Inside timeout");
}, 2000);
console.log("End");
And the output was:
Start
End
Inside timeout
At first… this completely confused me.
Because I expected code to run: top to bottom.
But Node.js had other plans. And that’s when I discovered one of the MOST important concepts in backend development:
Asynchronous Code
Because Node.js is built around async behavior. And honestly… once this concept clicks…Node.js architecture starts making WAY more sense.
Why Async Code Even Exists
Let’s think about something simple. Suppose your app is reading a huge file.
Example:
movie.mp4
Now imagine Node.js stops EVERYTHING while waiting for that file.
What happens?
entire application freezes.
No other requests.
No responses.
Nothing.
That would be terrible for servers handling thousands of users.
So instead…
Node.js uses:
Asynchronous execution
Meaning: “Start task now… continue other work… finish later.”
Real-Life Analogy
Imagine ordering coffee.
You don’t stand inside kitchen watching coffee being made.
You:
place order
continue doing other things
collect coffee later
That’s basically async behavior.
Node.js and Non-Blocking Behavior
This is one reason Node.js became popular. Node.js avoids blocking operations whenever possible.
Instead of waiting… it keeps handling other tasks. That makes applications feel faster and more scalable.
File Reading Example
This example made async code click for me.
const fs = require("fs");
fs.readFile("demo.txt", "utf8", (err, data) => {
console.log(data);
});
console.log("File reading started");
What Happens Here?
Node.js does NOT stop execution.
Instead:
Start file reading
↓
Continue running code
↓
File finishes later
↓
Callback executes
That’s asynchronous flow.
Why This Feels Strange Initially
Because beginners expect:
Line 1
Line 2
Line 3
strictly in order.
But async code introduces: delayed execution.
Some code executes later when task completes.
Callback-Based Async Execution
Initially… callbacks were the main way Node.js handled async behavior.
Example:
fs.readFile("demo.txt", "utf8", (err, data) => {
console.log(data);
});
This function:
(err, data) => {
console.log(data);
}
is the callback.
Node.js says: “Once file reading finishes… execute this function.”
Callback Execution Flow
Read File Request
↓
Node.js Continues Running
↓
File Finishes Loading
↓
Callback Executes
This pattern appears EVERYWHERE in Node.js.
Why Callbacks Were Useful
Callbacks solved a huge problem: waiting.
Instead of freezing application… Node.js could continue serving other tasks. That’s powerful for backend systems.
But Then Came The Problem…
As applications became larger… callbacks started becoming messy.
Very messy.
Nested Callbacks
Example:
loginUser(() => {
getProfile(() => {
getPosts(() => {
getComments(() => {
console.log("Everything loaded");
});
});
});
});
And honestly… this quickly becomes painful to read.
Callback Hell
This structure became known as:
Callback Hell
Because code keeps drifting:
right
right
right
Creating giant pyramids.
Visual Representation
Callback
↓
Nested Callback
↓
Nested Callback
↓
Nested Callback
Hard to debug.
Hard to maintain.
Hard to scale.
Another Big Problem
Error handling became ugly too.
Example:
fs.readFile("demo.txt", "utf8", (err, data) => {
if (err) {
console.log(err);
} else {
console.log(data);
}
});
Now imagine multiple nested operations. Error handling becomes chaotic very quickly.
This Is Why Promises Were Introduced
Promises solved readability problems. Instead of deeply nested callbacks… code became flatter and cleaner.
And honestly…
Promises made async JavaScript MUCH easier to manage.
What is a Promise?
A Promise is basically: an object representing future completion of an async task.
Sounds complex. But the idea is simple.
Real Mental Model
Promise says: “I don’t have result right now… but I WILL eventually.”
That’s it.
Promise States
A Promise can be:
Pending
Fulfilled
Rejected
Pending
Task still running.
Fulfilled
Task completed successfully.
Rejected
Task failed.
Simple Promise Example
const promise = new Promise((resolve, reject) => {
resolve("Success");
});
Here:
resolve()
means: operation succeeded.
And:
reject()
means: operation failed.
Using .then()
Promises use:
.then()
for successful results.
Example:
promise.then((data) => {
console.log(data);
});
Output:
Success
Handling Errors with .catch()
promise
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log(err);
});
Cleaner error handling. Less nesting. Much more readable.
Reading Files with Promises
Modern Node.js supports promise-based file reading.
Example:
const fs = require("fs/promises");
fs.readFile("demo.txt", "utf8")
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log(err);
});
Why Promises Feel Better
Compare this mentally.
Callback Style
Callback inside callback
Promise Style
Step-by-step chain
Promises flatten async logic. That readability matters A LOT in real applications.
Promise Flow
Async Task Starts
↓
Promise Created
↓
Success → then()
Failure → catch()
Another Important Insight
Promises didn’t remove async behavior.
They improved: async code organization.
That distinction matters.
Why Async Code Is So Important in Node.js
Node.js servers constantly handle:
database queries
API requests
file operations
authentication
uploads
These tasks take time. Async behavior prevents server from freezing while waiting. That’s one reason Node.js scales so well.
Small But Powerful Realization
At first I thought async code was just “weird execution order.”
But later I realized: async code is what allows Node.js to handle many operations efficiently.
That’s much deeper.
Common Beginner Confusion
People often think: async means parallel threads.
Not exactly.
Node.js mainly uses: event-driven non-blocking architecture.
Which is different from traditional multithreading models.
Callback vs Promise Readability
This comparison changed everything for me.
Callbacks
Deep nesting
Harder error handling
Messy flow
Promises
Cleaner chaining
Better readability
Centralized error handling
Why Promises Became Popular
Because developers needed cleaner async code. As applications grew larger… callback hell became a serious maintainability problem.
Promises solved a huge part of that.
Another Important Realization
Even modern:
async/await
is actually built ON TOP of Promises. So understanding Promises first is VERY important.
Assignment Practice
Try these yourself:
1. Use setTimeout()
2. Read file using callback
3. Create nested callbacks
4. Create simple Promise
5. Use .then() and .catch()
6. Compare readability carefully
That comparison is where understanding becomes real.
Final Understanding
At first, async code feels confusing because execution stops behaving “normally.”
But underneath everything…
the goal is simple: avoid blocking the application while waiting for slow tasks.
Callbacks were the first major solution. Promises improved the structure and readability of that solution.
And honestly…
once async thinking clicks…
Node.js starts feeling WAY more powerful.

