Back to blog
JavaScript

Async Code in Node.js: Callbacks and Promises

May 10, 2026 4 min read

Async Code in Node.js: Callbacks and Promises

Why Async Code Exists in Node.js

Node.js is built on a single-threaded event-driven architecture.
It uses asynchronous programming so that slow operations do not block the entire application.

Examples of slow operations:

  • Reading files

  • Database queries

  • API calls

  • Network requests

  • Timers

Without async execution, every request would wait for the previous task to finish.


Synchronous vs Asynchronous Execution

Synchronous Code

const fs = require("fs");

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

console.log(data);
console.log("Finished");

Flow

Read file completely
↓
Print file content
↓
Print "Finished"

The program waits until the file reading is completed.


Problem With Blocking Code

Imagine a server handling 10,000 users.

If one file read blocks the thread:

  • Other users must wait

  • API becomes slow

  • Throughput decreases

Node.js avoids this using asynchronous execution.


Callback-Based Async Execution

Basic Callback Example

const fs = require("fs");

console.log("Start");

fs.readFile("notes.txt", "utf-8", (error, data) => {
  if (error) {
    console.log("Error:", error);
    return;
  }

  console.log(data);
});

console.log("End");


Output

Start
End



Step-by-Step Callback Flow

What Happens Internally

Diagram: sequenceDiagram

Understanding the Callback

Structure

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

Parameters

ParameterMeaning
errorContains error if operation fails
dataContains successful result

Why Callbacks Were Used

Before promises existed:

  • Callbacks were the standard async mechanism

  • Node.js APIs were designed around them

  • Event-driven systems depended heavily on callbacks


Problems With Nested Callbacks

When multiple async operations depend on each other, callbacks become deeply nested.

This is called:

  • Callback Hell

  • Pyramid of Doom


Nested Callback Example

const fs = require("fs");

fs.readFile("user.txt", "utf-8", (err, userData) => {
  if (err) {
    console.log(err);
    return;
  }

  fs.readFile("orders.txt", "utf-8", (err, orderData) => {
    if (err) {
      console.log(err);
      return;
    }

    fs.readFile("payments.txt", "utf-8", (err, paymentData) => {
      if (err) {
        console.log(err);
        return;
      }

      console.log(userData);
      console.log(orderData);
      console.log(paymentData);
    });
  });
});


Problems in This Code

1. Poor Readability

Code moves toward the right repeatedly.


2. Difficult Error Handling

Each level needs separate error checks.


3. Hard to Maintain

Adding more async operations increases complexity.


4. Difficult Debugging

Tracing execution becomes confusing.


Callback Execution Chain

Diagram: graph TD

Promise-Based Async Handling

Promises were introduced to solve callback problems.

A promise represents a future value.

It can be:

StateMeaning
PendingOperation still running
FulfilledOperation succeeded
RejectedOperation failed

Promise Lifecycle Flow

Diagram: stateDiagram-v2

File Reading Using Promises

Using fs/promises

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

console.log("Start");

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

console.log("End");


Promise Flow

Diagram: sequenceDiagram

Benefits of Promises

1. Better Readability

Callbacks become flat chains.


2. Centralized Error Handling

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

One catch block can handle multiple failures.


3. Easier Chaining

fs.readFile("user.txt", "utf-8")
  .then((userData) => {
    return fs.readFile("orders.txt", "utf-8");
  })
  .then((orderData) => {
    return fs.readFile("payments.txt", "utf-8");
  })
  .then((paymentData) => {
    console.log(paymentData);
  })
  .catch((error) => {
    console.log(error);
  });


Callback vs Promise Readability

Callback Version

fs.readFile("a.txt", "utf-8", (err, dataA) => {
  fs.readFile("b.txt", "utf-8", (err, dataB) => {
    fs.readFile("c.txt", "utf-8", (err, dataC) => {
      console.log(dataC);
    });
  });
});


Promise Version

fs.readFile("a.txt", "utf-8")
  .then(() => {
    return fs.readFile("b.txt", "utf-8");
  })
  .then(() => {
    return fs.readFile("c.txt", "utf-8");
  })
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.log(error);
  });


Comparison Table

FeatureCallbacksPromises
ReadabilityPoor in nested operationsMuch cleaner
Error HandlingRepeated checksCentralized
ChainingDifficultEasy
MaintainabilityHardBetter
DebuggingComplicatedEasier

Important Promise Methods

MethodPurpose
.then()Handles success
.catch()Handles errors
.finally()Runs always

Example With .finally()

fs.readFile("notes.txt", "utf-8")
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.log(error);
  })
  .finally(() => {
    console.log("Operation completed");
  });


Real-World Use Cases

Promises are heavily used in:

  • Database drivers

  • HTTP requests

  • Authentication systems

  • Payment gateways

  • Cloud APIs

  • Background jobs


Key Takeaways

ConceptSummary
Async CodePrevents blocking operations
CallbacksOlder async pattern
Callback HellDeep nested callback structure
PromisesCleaner async handling mechanism
.then()Success handler
.catch()Error handler
Promises AdvantageBetter readability and maintainability

Callbacks are still present in older Node.js APIs and many legacy applications.

Promises became the modern standard because they:

  • Improve readability

  • Simplify async flows

  • Reduce nesting

  • Improve error handling

Modern Node.js development heavily uses:

  • Promises

  • async/await (built on top of promises)

Understanding callbacks first is important because promises internally solve many callback-related problems.


0 Comments

Sign in to join the conversation

No comments yet. Be the first to comment!