Skip to main content

Command Palette

Search for a command to run...

Async Code in Node.js: Callbacks vs Promises (Explained with Real-Life Examples)

Ever ordered food at a restaurant and didn’t just stand at the counter waiting? That’s exactly how Node.js works.

Updated
4 min read
Async Code in Node.js: Callbacks vs Promises (Explained with Real-Life Examples)

A Simple Real Life Story

Imagine you go to a restaurant:

  • You place an order

  • The chef starts cooking

  • Instead of waiting there doing nothing

  • You go sit, talk, or use your phone

When your food is ready, the waiter calls you.

That is asynchronous programming.

Why Async Code Exists in Node.js

Node.js is single-threaded, which means:

  • It can do one task at a time

  • It avoids blocking execution

Problem (Synchronous Way)

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

In this case, Node.js waits until the file is fully read. Everything else is blocked.

Solution (Asynchronous Way)

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

Here, Node.js continues executing other tasks. When the file is ready, the callback runs.

Understanding Callbacks (Step by Step)

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

Example:

console.log("Start");

setTimeout(() => {
  console.log("Inside Callback");
}, 2000);

console.log("End");

Output:

Start
End
Inside Callback

Callback Flow Diagram

Image

Flow:

  1. Code runs line by line

  2. Async task moves to background

  3. Callback waits in queue

  4. Event loop executes it later

The Problem: Callback Hell

Consider a situation where each step depends on the previous one:

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

Problems:

  • Difficult to read

  • Difficult to debug

  • Deep nesting (pyramid structure)

  • Complex error handling

Enter Promises

Instead of relying on nested callbacks, Promises provide a cleaner way to handle asynchronous operations.

What is a Promise?

A Promise is an object that represents the eventual completion or failure of an asynchronous operation.

Promise States

Image
  • Pending: Initial state

  • Fulfilled: Operation completed successfully

  • Rejected: Operation failed

Using Promises

Example:

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

fs.readFile("file.txt")
  .then((data) => {
    console.log(data.toString());
  })
  .catch((err) => {
    console.error(err);
  });

Cleaner Code with Promises

fs.readFile("file1.txt")
  .then(data1 => fs.readFile("file2.txt"))
  .then(data2 => fs.readFile("file3.txt"))
  .then(data3 => console.log("All files read"))
  .catch(err => console.error(err));

Callback vs Promise

Feature Callback Promise
Readability Poor Clean
Error Handling Complex Simple
Chaining Difficult Easy
Debugging Hard Better

Why Promises Are Better

  • Avoid deeply nested code

  • Improve readability

  • Simplify error handling

  • Allow chaining of multiple async operations

  • Serve as a foundation for async/await

Callbacks were the initial approach to handling asynchronous operations in Node.js.

Promises improved the structure and clarity of async code, making it easier to write and maintain.

Summary

The article explains asynchronous programming using a simple restaurant analogy, where you place an order and occupy yourself with other activities until your food is ready. This mirrors how asynchronous code works in Node.js, which is single-threaded and processes one task at a time without blocking execution. In synchronous programming, like reading a file, Node.js waits and blocks other operations until the task completes. Asynchronous programming, however, allows Node.js to continue executing other tasks while waiting for a file operation to complete, using callbacks to handle the result once it's ready.

The article further discusses the challenges of using callbacks, such as "callback hell," which makes code difficult to read, debug, and manage due to deep nesting and complex error handling. It introduces Promises as a cleaner alternative for handling asynchronous operations. Promises represent the eventual completion or failure of an operation and improve code readability, error handling, and chaining of multiple asynchronous tasks. Promises provide a foundation for async/await, offering a more structured and maintainable approach to asynchronous programming compared to callbacks.