Skip to main content

Command Palette

Search for a command to run...

Async Code in Node.js: Callbacks and Promises

Updated
7 min read

Description:
Learn how asynchronous code works in Node.js using callbacks and promises. Understand callback flow, callback hell, promise chaining, and modern async programming with practical examples

Introduction:

Imagine you order food at a restaurant.You place your order, and instead of standing at the counter waiting for your food, you sit down, talk with friends, check your phone, or do other things while the kitchen prepares your meal.That is exactly how asynchronous programming works in Node.js.

Node.js does not like wasting time waiting for slow operations such as:

  • Reading files

  • Fetching data from databases

  • Calling APIs

  • Uploading images

  • Handling network requests

Instead, Node.js starts the task and continues doing other work until the result is ready.

This ability makes Node.js extremely fast and scalable.

Why Async Code Exists in Node.js:

Node.js is built on a single-threaded event-driven architecture.

That sounds complicated, but the idea is simple "Node.js tries to avoid blocking operations."

For example, imagine reading a large file from disk.

If Node.js waited for the file to fully load before doing anything else, the entire server would freeze temporarily.

That would be terrible for performance.

Instead, Node.js uses asynchronous operations so other users and requests can continue working smoothly.

A Real-World File Reading Scenario:

Suppose you have a file called data.txt.

You want Node.js to read it.

Synchronous Way (Blocking):

const fs = require("fs");

const data = fs.readFileSync("data.txt", "utf-8");

console.log(data);

console.log("Program Finished");

Problem:

Node.js will stop everything until the file is fully read.

If the file is huge, the application becomes slow.

Asynchronous File Reading with CallBacks:

Now let’s use asynchronous code.

const fs = require("fs");

fs.readFile("data.txt", "utf-8", (err, data) => {
if (err) {
console.log("Error reading file");
return;
}

console.log(data);
});

console.log("Program Finished");

Understanding Callback Flow Step-by-Step:

This is where many beginners get confused.

Step 1: Node.js Starts Reading the File:

fs.readFile(...)

Node.js sends the task to the system.

Step 2: Node.js Does NOT Wait:

Immediately after starting the file read operation:

console.log("Program Finished");

gets executed.

Step 3: File Reading Completes Later:

When the file is finally ready, Node.js executes this function:

(err, data) => {
console.log(data);
}

This function is called a callback function.

What is a Callback?:

A callback is simply a function passed into another function to be executed later.

function greet(name, callback) {
console.log("Hello " + name);

callback();
}

function afterGreeting() {
console.log("Greeting Completed");
}

greet("Hammad", afterGreeting);

Output:

Hello Hammad
Greeting Completed

Why Callbacks Became Popular in Node.js:

Callbacks were widely used because they allowed Node.js to handle:

  • File operations

  • Database queries

  • API requests

  • Timers

  • Network communication

without blocking the main thread.

This made Node.js highly efficient for backend development.

The Big Problem: Callback Hell

Callbacks work well initially.

But problems appear when multiple async tasks depend on each other.

getUser(userId, (user) => {

getPosts(user.id, (posts) => {

getComments(posts[0].id, (comments) => {

getLikes(comments[0].id, (likes) => {

console.log(likes);

});

});

});

});

Why is This Bad?

This structure creates:

  • Deep nesting

  • Difficult debugging

  • Poor readability

  • Hard maintenance

  • Error-handling complexity

This issue became known as:

“Callback Hell”

or sometimes:

“Pyramid of Doom”

Because the code shape looks like a pyramid.

Beginner Mistakes with Callbacks:

1. Forgetting Error Handling

Bad:

fs.readFile("data.txt", (err, data) => {
console.log(data);
});

Better:

if (err) {
console.log(err);
return;
}

2. Excessive Nesting:

Avoid deeply nested callback structures.

3. Mixing Sync and Async Code Incorrectly

Many beginners expect async code to run in order automatically.But async operations complete later.

Enter Promises: A Cleaner Solution

To solve callback hell, JavaScript introduced Promises.

A Promise represents: A value that may be available now, later, or never.

Think of a promise like ordering food online.

  • Pending → Food is being prepared

  • Resolved → Food delivered successfully

  • Rejected → Delivery failed

Promise-Based File Reading Example

const fs = require("fs").promises;

fs.readFile("data.txt", "utf-8")
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log(err);
});

Understanding Promise Flow

.then()

Runs when the operation succeeds.

.catch()

Runs when the operation fails.

This creates cleaner and more readable code.

Callback vs Promise Comparison:

Feature Callbacks Promises

Readability

Harder in complex tasks

Cleaner

Error Handling

Manual

Centralized

Nesting

Deep nesting possible

Better chaining

Maintenance

Difficult

Easier

Scalability

Messy in large apps

Better for large systems

Promise Chaining:

Promises allow sequential async operations without nesting.

getUser(userId)
.then((user) => {
return getPosts(user.id);
})
.then((posts) => {
return getComments(posts[0].id);
})
.then((comments) => {
console.log(comments);
})
.catch((err) => {
console.log(err);
});

This is much cleaner than callback hell.

Benefits of Promises:

  1. Better Readability

Code becomes easier to understand.

2.Improved Error Handling

One .catch() can handle multiple async errors.

3.Easier Maintenance

Large applications become manageable.

4.Better Chaining

Async tasks can run in sequence elegantly.

5.Foundation for Async/Await

Modern JavaScript uses async/await, which is built on promises.

Real-World Applications of Async Code:

Asynchronous programming powers almost every modern application.

Examples include:

Chat applications

Social media platforms

Online gaming servers

Streaming services

E-commerce systems

Banking systems

Cloud applications

Companies like:

Netflix

PayPal

LinkedIn

use Node.js because of its asynchronous and scalable architecture.

Pro Tip: Use Async/Await in Modern Projects:

Although promises are powerful, modern developers often use:

async/await

because it looks more like normal synchronous code.

Example:

const fs = require("fs").promises;

async function readFileData() { try { const data = await fs.readFile("data.txt", "utf-8");

console.log(data);

} catch (err) {
console.log(err);
}

}

readFileData();

This is currently the most popular approach in professional Node.js development.

Performance and Scalability Advantages

Async programming helps Node.js:

Handle thousands of users simultaneously

Improve server responsiveness

Reduce blocking operations

Scale efficiently

This is one reason why Node.js is popular for:

APIs

Microservices

Realtime applications

Streaming platforms

Common Beginner Confusions :

“Why does my console output appear out of order?”

Because asynchronous operations complete later.

“Why doesn’t return work inside callbacks?”

Because the callback runs after the outer function has already finished.

“Are promises faster than callbacks?”

Not necessarily faster.

Promises mainly improve:

Code quality

Maintainability

Readability

Learning Roadmap for Async JavaScript

Here’s a smart progression path:

Learn synchronous JavaScript

Understand callbacks

Practice async file handling

Learn promises deeply

Master async/await

Study event loop and microtasks

Build real backend projects

Tools & Resources:

Useful tools for learning Node.js async programming:

Node.js Official Website

MDN JavaScript Promises Guide

JavaScript Info Async Tutorial

Career Opportunities

Understanding async programming is essential for roles like:

Backend Developer

Full Stack Developer

API Engineer

Cloud Engineer

DevOps Engineer

Realtime Application Developer

Async programming is one of the core skills recruiters expect from modern JavaScript developers.

Frequently Asked Questions (FAQ):

Is callback programming outdated?

No. Many older systems still use callbacks.

However, promises and async/await are preferred today.

Are promises difficult to learn?

Initially yes, but with practice they become intuitive.

Should beginners learn callbacks first?

Absolutely.

Understanding callbacks helps you truly understand async JavaScript.

What is better: Promises or Async/Await?

async/await is generally cleaner and easier to read.

But internally, it still uses promises.

Conclusion

Asynchronous programming is one of the most important concepts in Node.js.

Without async behavior, Node.js would lose much of its speed and scalability advantages.

Callbacks introduced developers to non-blocking programming, but they often created messy and difficult-to-maintain code.

Promises solved many of those problems by making async code cleaner, more readable, and easier to manage.

Today, modern Node.js development heavily relies on promises and async/await.

If you truly master async programming, you’ll become significantly more confident as a JavaScript and backend developer.