Skip to main content

Command Palette

Search for a command to run...

Async Code in Node.js: Callbacks and Promises

Published
4 min read

When you first start working with Node.js, one concept keeps showing up everywhere: asynchronous code.

At first glance, it feels confusing — why not just run code line by line like normal programs?

Let’s break it down step by step.

Why Async Code Exists in Node.js

Node.js is built on a single-threaded, non-blocking event loop architecture.

This means:

  • It can handle many operations at once

  • But it does not create a new thread for each task

Now imagine this scenario:

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

This is blocking (synchronous):

  • Node waits for the file to be read

  • Nothing else happens during that time

Problem

If file reading takes time:

  • Your server is stuck

  • Other users have to wait

Solution: Asynchronous Code

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

console.log("Done");

Now:

  • Node does not wait

  • It continues execution

  • Handles the result later

Real Scenario: File Reading

Let’s understand the flow properly.

Code:

console.log("Start");

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

console.log("End");

Step-by-Step Execution

  1. "Start" is printed

  2. fs.readFile is sent to the system (async task)

  3. Node does NOT wait

  4. "End" is printed

  5. Once file reading completes → callback executes

  6. "File Data" is printed

Output Order

Start
End
File Data: ...

This is the core idea of async in Node.js.

Callback-Based Async Execution

A callback is simply:

A function passed as an argument to another function, executed later.

Example:

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

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

Key Idea

  • You give control to another function

  • It calls you back when work is done

Problem: Callback Hell

Now let’s chain multiple async operations:

fs.readFile("file1.txt", "utf-8", (err, data1) => {
  fs.readFile("file2.txt", "utf-8", (err, data2) => {
    fs.readFile("file3.txt", "utf-8", (err, data3) => {
      console.log(data1, data2, data3);
    });
  });
});

Issues

  • Deep nesting (pyramid shape)

  • Hard to read

  • Hard to debug

  • Error handling becomes messy

This is called:

Callback Hell (or Pyramid of Doom)

Promise-Based Async Handling

Promises were introduced to fix these problems.

What is a Promise?

A Promise represents:

A value that will be available in the future

States of a Promise

  • Pending

  • Resolved (fulfilled)

  • Rejected

Basic Example

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

promise.then((data) => {
  console.log(data);
});

Rewriting File Example Using Promises

Using fs.promises:

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

fs.readFile("file1.txt", "utf-8")
  .then((data1) => {
    return fs.readFile("file2.txt", "utf-8")
      .then((data2) => {
        return fs.readFile("file3.txt", "utf-8")
          .then((data3) => {
            console.log(data1, data2, data3);
          });
      });
  })
  .catch((err) => {
    console.log(err);
  });

Better Version (Flat Chain)

fs.readFile("file1.txt", "utf-8")
  .then((data1) => {
    console.log(data1);
    return fs.readFile("file2.txt", "utf-8");
  })
  .then((data2) => {
    console.log(data2);
    return fs.readFile("file3.txt", "utf-8");
  })
  .then((data3) => {
    console.log(data3);
  })
  .catch((err) => {
    console.log(err);
  });

Benefits of Promises

1. Better Readability

  • No deep nesting

  • Linear flow

2. Centralised Error Handling

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

Instead of handling errors at every level.

3. Easier Composition

You can chain multiple async operations cleanly.

4. Foundation for Async/Await

Promises enable:

const data = await fs.readFile("file.txt", "utf-8");

Callback vs Promise Comparison

Feature Callbacks Promises
Readability Poor (nested) Clean (chainable)
Error Handling Scattered Centralized (.catch)
Debugging Difficult Easier
Scalability Poor Better
Structure Pyramid-like Linear flow

Final Insight

  • Callbacks are low-level primitives

  • Promises are structured abstractions

In modern Node.js:

You should prefer Promises (and async/await) over callbacks