Skip to main content

Command Palette

Search for a command to run...

Async Code in Node.js: Callbacks and Promises

Updated
4 min read
Async Code in Node.js: Callbacks and Promises
P

Software Engineer | Passionate about Web Development, DSA & Problem Solving. I write simple, practical tech blogs to help developers learn and grow. Exploring JavaScript, C++, Backend & Modern Web Technologies.

Introduction

Node.js is designed to handle multiple operations efficiently using a non-blocking, asynchronous architecture. Unlike traditional systems where tasks execute one after another, Node.js allows tasks like file reading, database queries, and API calls to run in the background.

But this raises an important question:

How does Node.js manage operations that take time without stopping execution?

The answer lies in asynchronous programming, primarily using:

  • Callbacks (traditional approach)

  • Promises (modern approach)

Understanding these concepts is crucial because they form the foundation of how real-world Node.js applications work.

1. Why Async Code Exists in Node.js

Node.js runs on a single thread, meaning it can execute only one task at a time.

If it used only synchronous (blocking) code:

  • One slow operation would block the entire server

  • All users would experience delays

Real-World Scenario (File Reading)

Imagine a server reading a large file:

  • If blocking → server waits

  • If non-blocking → server continues handling other users

Example (Blocking):

const fs = require("fs");

const data = fs.readFileSync("file.txt", "utf8");
console.log(data);

Explanation:

  • Execution stops until file is fully read

  • No other task can run

Example (Async):

const fs = require("fs");

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

Explanation:

  • File reading happens in background

  • Server continues executing other code

  • Callback runs when file is ready

2. Callback-Based Async Execution

A callback is a function passed as an argument to another function and executed later.

Example:

function fetchData(callback) {
    setTimeout(() => {
        callback("Data received");
    }, 1000);
}

fetchData((result) => {
    console.log(result);
});

Explanation :

  1. fetchData is called

  2. setTimeout simulates async task

  3. Callback is stored

  4. After 1 second → callback executes

  5. "Data received" is printed

Output:

Data received

Key Idea:

  • Callback runs after async task completes

  • Allows non-blocking execution

3. Problems with Nested Callbacks (Callback Hell)

Callbacks work well initially, but become problematic when multiple async tasks depend on each other.

Example (Nested Callbacks):

setTimeout(() => {
    console.log("Step 1");

    setTimeout(() => {
        console.log("Step 2");

        setTimeout(() => {
            console.log("Step 3");
        }, 1000);

    }, 1000);

}, 1000);

Explanation:

  • Each step depends on previous one

  • Code becomes deeply nested

  • Hard to read and maintain

Output:

Step 1
Step 2
Step 3

Problems:

  • Poor readability

  • Difficult debugging

  • Error handling becomes complex

This situation is called callback hell.

4. Promise-Based Async Handling

Promises were introduced to solve callback problems.

A Promise represents a future value.

Example:

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("Data received");
    }, 1000);
});

promise.then(result => console.log(result));

Explanation :

  • Promise starts in pending state

  • After 1 second → resolve() is called

  • .then() executes with result

Output:

Data received

Key Idea:

  • Cleaner structure than callbacks

  • Separates success and failure

5. Promise Chaining (Replacing Nested Callbacks)

Promises allow chaining multiple async operations.

Example:

function step1() {
    return Promise.resolve("Step 1");
}

function step2() {
    return Promise.resolve("Step 2");
}

function step3() {
    return Promise.resolve("Step 3");
}

step1()
    .then(result => {
        console.log(result);
        return step2();
    })
    .then(result => {
        console.log(result);
        return step3();
    })
    .then(result => {
        console.log(result);
    });

Explanation:

  • Each .then() waits for previous step

  • No nesting required

  • Code flows linearly

Output:

Step 1
Step 2
Step 3

6. Benefits of Promises

Promises improve asynchronous programming in multiple ways.

Better Readability

Linear flow instead of nested structure

Easy Error Handling

promise
    .then(result => console.log(result))
    .catch(err => console.log(err));

Chaining Support

Multiple async tasks handled sequentially

Foundation for Async/Await

Promises are the base of modern async syntax

7. Callback vs Promise Readability

Callback Style:

doTask1(() => {
    doTask2(() => {
        doTask3(() => {
            console.log("Done");
        });
    });
});

Promise Style:

doTask1()
    .then(doTask2)
    .then(doTask3)
    .then(() => console.log("Done"));

Explanation:

  • Callback → nested and complex

  • Promise → flat and readable

Conclusion

Asynchronous programming is the backbone of Node.js.

  • Callbacks introduced async handling but led to complexity

  • Promises improved structure, readability, and error handling

By understanding both:

  • You can maintain legacy code (callbacks)

  • Write modern scalable applications (promises)