Skip to main content

Command Palette

Search for a command to run...

Async Code in Node.js: Callbacks and Promises

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

Why async code exists in Node.js

First, we have to understand why we need async behaviour in JavaScript

As we all know, JavaScript is a single-threaded language that runs or executes line by line.

Let's suppose you want to read or download your data. So what do you prefer? Do you want to wait for the data or do something else because it takes time, right?
Now, what should your app do while waiting? Just sit idle, or give you access to other features

  • Node.js is single-threaded

  • It cannot do many things at once, like Java or Python threads

  • So it uses asynchronous (non-blocking) behavior

This is why async code exists in NodeJs

Example:

const fs = require("fs");

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

console.log("File reading started...");
  1. readFile starts reading

  2. Node DOES NOT wait

  3. "File reading started..." prints first

  4. When file is ready → callback runs

This clearly shows async behavior

Callback-based async execution

  • A callback is just a function passed inside another function

  • It runs later when the task is completed

Example:

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

Problems with nested callbacks

Nested callbacks are okay, but when it goes deeper and deeper, It becomes very complex to understand and debug. which is known as callback hell. It looks like a pyramid structure

Example:

getUser(userId, (user) => {
  getPosts(user, (posts) => {
    getComments(posts, (comments) => {
      console.log(comments);
    });
  });
});
  • Hard to read and understand

  • Hard to debug

  • Too many nested functions

Promise-based async handling

A Promise represents a value that may be available now, later, or never.

A promise has three states:

  1. Pending

  2. Fulfilled

  3. Rejected

Example:

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

fs.readFile("data.txt", "utf8")
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.log(error);
  });

Here:

  • .then() handles success

  • .catch() handles errors

Promise looks cleaner and easier to understand

Benefits of promises

  1. Better Readability: Code looks cleaner and more organized.

  2. Better Error Handling: A single .catch() can handle errors from the entire chain.

  3. Avoids Callback Hell: No deep nesting of functions.

  4. Easier Maintenance: Future developers can understand the code more easily.

Conclusion

Callbacks were the original way to handle asynchronous operations in Node.js. They work well for simple tasks, but multiple nested callbacks can make code difficult to read and maintain.

Promises provide a cleaner and more structured way to manage asynchronous operations. They improve readability, simplify error handling, and help avoid callback hell.

Because of these advantages, promises are commonly used in modern Node.js applications.