Skip to main content

Command Palette

Search for a command to run...

Async Code in Node.js: Callbacks and Promises

Updated
โ€ข9 min read

Introduction

Modern applications constantly perform operations that take time:

  • Reading files

  • Fetching API data

  • Connecting to databases

  • Uploading images

  • Sending emails

  • Authentication

If JavaScript waited for every operation to complete before executing the next line, applications would become extremely slow.

This is why Node.js heavily depends on asynchronous programming.

In this article, we will deeply understand:

  • Why async code exists in Node.js

  • Callback-based asynchronous execution

  • Problems with nested callbacks

  • Promise-based async handling

  • Benefits of promises

  • Callback vs Promise readability

  • Real-world examples

By the end, you will clearly understand how Node.js handles asynchronous operations behind the scenes.


What is Asynchronous Programming?

Asynchronous programming means:

Executing long-running operations without blocking the main thread.

Instead of waiting for a task to finish, Node.js starts the operation and continues executing other code.

This makes applications:

  • Faster

  • More scalable

  • Better at handling multiple users


Why Async Code Exists in Node.js

Node.js uses a:

  • Single-threaded

  • Event-driven

  • Non-blocking architecture

This means JavaScript runs on a single thread.

If one operation blocks the thread, the entire server becomes unresponsive.

For example:

Imagine 100 users uploading files at the same time.

If Node.js handled everything synchronously:

  • One user would wait for another

  • Requests would become slow

  • Performance would drop badly

Instead, Node.js delegates slow tasks and continues executing other operations.

This is the core reason asynchronous programming exists.


Real-World Scenario

Suppose your application performs these steps:

  1. Read user data

  2. Connect to database

  3. Fetch orders

  4. Send response

All these operations take time.

Node.js does not stop the entire application while waiting.

Instead:

  • It starts the operation

  • Continues executing other code

  • Executes the callback later when result arrives


Blocking vs Non-Blocking Code

Understanding this difference is extremely important.


Blocking Code Example

const fs = require("fs");

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

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

What Happens Here?

  1. File reading starts

  2. JavaScript waits until reading completes

  3. Data gets printed

  4. Next line executes

This blocks execution.


Suggested Image Placement

๐Ÿ“Œ Place an image here:

Image Idea

"Blocking vs Non-Blocking Execution Timeline"

What the image should show

Blocking Flow

Read File โ†’ Wait โ†’ Print Data โ†’ Continue

Non-Blocking Flow

Start File Read โ†’ Continue Execution โ†’ Callback Executes Later

This image helps readers visually understand execution flow.


Non-Blocking Code Example

const fs = require("fs");

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

console.log("Finished");

Execution Flow

Step 1

Node.js starts reading the file.


Step 2

Instead of waiting, it continues executing next lines.


Step 3

console.log("Finished")

executes immediately.


Step 4

When file reading completes, callback executes.

Final Output:

Finished
File Content

This is asynchronous execution.


How Node.js Handles Async Operations

Internally, Node.js uses:

  • Event Loop

  • Callback Queue

  • System APIs

  • Thread Pool

When async operations start:

  1. Node.js delegates the task

  2. Continues executing remaining code

  3. Completed callbacks move into queue

  4. Event loop executes callbacks


Suggested Image Placement

Image Idea

"Node.js Async Architecture"

Diagram Structure

Main Thread
     โ†“
Async Operation
     โ†“
System APIs / Thread Pool
     โ†“
Callback Queue
     โ†“
Event Loop
     โ†“
Execute Callback

This image is extremely useful because readers struggle to visualize async execution internally.


Callback-Based Async Execution

Callbacks were the original solution for asynchronous programming in JavaScript.

A callback is:

A function passed into another function to execute later.


Basic Callback Example

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

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

Step-by-Step Callback Flow

Step 1

fetchData() starts.


Step 2

setTimeout() waits for 2 seconds.


Step 3

Callback function executes.


Step 4

Data gets printed.

Output:

Data received

Suggested Image Placement

Diagram Structure

Main Function
     โ†“
Async Task Starts
     โ†“
Continue Execution
     โ†“
Task Completes
     โ†“
Callback Queue
     โ†“
Event Loop
     โ†“
Execute Callback

This image perfectly explains how callbacks work internally.


Real-World File Reading Example

const fs = require("fs");

console.log("Start");

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

  console.log(data);
});

console.log("End");

Output

Start
End
User File Content

Notice:

End prints before file content.

This proves Node.js does not block execution.


Problems with Nested Callbacks

Callbacks are fine for small applications.

But real-world applications often need multiple asynchronous operations.

Example:

  1. Get user data

  2. Fetch orders

  3. Process payment

  4. Send email

Using callbacks everywhere creates deeply nested code.


Callback Hell Example

getUser(userId, (user) => {
  getOrders(user.id, (orders) => {
    getPayment(orders[0], (payment) => {
      sendEmail(payment, () => {
        console.log("Email sent");
      });
    });
  });
});

Why Callback Hell is a Problem

1. Poor Readability

Code becomes hard to understand.


2. Difficult Debugging

Finding errors becomes painful.


3. Pyramid Structure

The code keeps moving right.


4. Hard Maintenance

Adding new features becomes difficult.


Suggested Image Placement

๐Ÿ“Œ Place an image here:

Image Idea

"Pyramid of Doom"

Diagram Structure

Task1(
  Task2(
    Task3(
      Task4(
        Task5()
      )
    )
  )
)

Use arrows or indentation to visually show increasing nesting.

This image makes callback hell instantly understandable.


Introduction to Promises

Promises were introduced to solve callback problems.

A Promise represents:

A future value that may succeed or fail.

Promises provide:

  • Cleaner syntax

  • Better readability

  • Easier chaining

  • Better error handling


Promise States

Every promise has 3 states.

1. Pending

Operation still running.


2. Fulfilled

Operation completed successfully.


3. Rejected

Operation failed.


Creating a Promise

const promise = new Promise((resolve, reject) => {
  const success = true;

  if (success) {
    resolve("Operation successful");
  } else {
    reject("Operation failed");
  }
});

Using a Promise

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

File Reading Using Promises

Modern Node.js supports promise-based APIs.

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

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

Promise Flow Step-by-Step

Step 1

readFile() starts.


Step 2

Promise enters pending state.


Step 3

If successful:

.then()

executes.


Step 4

If failed:

.catch()

executes.


Comparing Callback vs Promise

Callback Version

getUser(userId, (user) => {
  getOrders(user.id, (orders) => {
    getPayment(orders[0], (payment) => {
      console.log(payment);
    });
  });
});

Promise Version

getUser(userId)
  .then((user) => {
    return getOrders(user.id);
  })
  .then((orders) => {
    return getPayment(orders[0]);
  })
  .then((payment) => {
    console.log(payment);
  })
  .catch((err) => {
    console.log(err);
  });

Benefits of Promises

Cleaner Code

Promises avoid excessive nesting.


Better Readability

Execution flow becomes linear.


Centralized Error Handling

Single .catch() handles multiple errors.


Easier Chaining

Async operations connect naturally.


Better Scalability

Promises work better in large applications.


Real-World Analogy

Imagine ordering food online.

Callback Style

You repeatedly call the restaurant:

  • Is my order accepted?

  • Is it prepared?

  • Is it shipped?

  • Is it arriving?

Messy and repetitive.


Promise Style

You simply wait for notification updates.

Much cleaner.


Async/Await (Modern Approach)

Today most applications use:

  • Promises

  • Async/Await

because they are easier to read.


Async/Await 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();

Async/Await is built on top of promises.


Callback vs Promise Comparison Table

Feature Callbacks Promises
Readability Difficult Cleaner
Nesting High Low
Error Handling Repetitive Centralized
Chaining Difficult Easy
Maintainability Hard Better
Scalability Poor Excellent

Important Interview Questions

What is asynchronous programming?

Executing operations without blocking the main thread.


What is a callback?

A function passed into another function to execute later.


What is callback hell?

Deeply nested callbacks creating unreadable code.


What are promises?

Objects representing future success or failure of async operations.


What are promise states?

  • Pending

  • Fulfilled

  • Rejected


Why are promises better than callbacks?

  • Better readability

  • Cleaner structure

  • Easier chaining

  • Centralized error handling


Final Thoughts

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

Without async behavior, Node.js would not efficiently handle:

  • APIs

  • Databases

  • File systems

  • Authentication

  • Real-time applications

Callbacks introduced asynchronous execution in JavaScript.

Promises improved it by making async code:

  • Cleaner

  • Easier to read

  • Easier to debug

  • Easier to scale

Mastering callbacks and promises is essential before learning:

  • Async/Await

  • Event Loop

  • Streams

  • WebSockets

  • Backend Architecture

Once you understand these concepts deeply, Node.js asynchronous programming becomes much easier to work with.