Skip to main content

Command Palette

Search for a command to run...

Async Code in Node.js: Callbacks and Promises

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

Learning web development in public. Writing simple, real-world explanations about web development concepts. Helping beginners understand why things work, not just how.

In this article, we’ll understand one of the most important concepts in Node.js — asynchronous code.

What we are going to learn

  • Why async code exists in Node.js

  • Callback-based async execution

  • Problems with nested callbacks

  • Promise-based async handling

  • Benefits of promises


What is Async Code and Why It Exists

Let’s start with a basic question:

Why doesn’t Node.js execute everything line by line like traditional programs?

Because Node.js is single-threaded.

This means:

  • It can run only one task at a time

  • But it still needs to handle multiple users, requests, file operations, and API calls

Now imagine this:

const data = readFileSync("largeFile.txt");
console.log(data);

If the file is large, Node.js will:

  • Stop execution

  • Wait until the file is fully read

  • Then move forward

This creates a problem:

While waiting, the server cannot handle other requests.


Solution: Asynchronous Code

Instead of waiting, Node.js uses async code.

The idea is simple:

Start a task, and when it finishes, notify me. Meanwhile, I will continue doing other work.

Example:

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

console.log("This runs first");

Output:

This runs first
(file content later)

Node.js does not block execution, which makes it fast and efficient.


Callback-Based Async Execution

The first approach to handling async operations in Node.js was callbacks.

A callback is simply a function passed as an argument to another function, which is executed later.

Example:

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

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

Flow:

  1. The async task starts

  2. Node.js continues execution

  3. Once the task finishes, the callback runs


Problems with Nested Callbacks (Callback Hell)

Now consider multiple dependent async operations:

loginUser((user) => {
getProfile(user, (profile) => {
getPosts(profile, (posts) => {
console.log(posts);
    });
  });
});

This structure is called callback hell.

Problems:

  • Code becomes deeply nested

  • Hard to read and understand

  • Debugging becomes difficult

  • Error handling is inconsistent

This pattern is often called the "pyramid of doom".


Promise-Based Async Handling

To solve the problems of callbacks, JavaScript introduced promises.

A promise represents a value that will be available in the future.

It has three states:

  • Pending

  • Resolved

  • Rejected


Example using Promise

function fetchData() {

return new Promise((resolve,reject) => {

setTimeout(() => {
resolve("Data received");
    },2000);
  });
}

fetchData()
.then((data) => {
console.log(data);
  })
.catch((err) => {
console.error(err);
  });

Chaining Promises

loginUser()
.then((user) =>getProfile(user))
.then((profile) =>getPosts(profile))
.then((posts) =>console.log(posts))
.catch((err) =>console.error(err));

This removes nesting and creates a clear, linear flow.


Benefits of Promises

1. Better readability

Code is flatter and easier to understand compared to nested callbacks.

2. Centralized error handling

Instead of handling errors at every step, a single .catch() can handle failures.

3. Easy chaining of async operations

Multiple async steps can be connected in a clean sequence.

4. Foundation for async/await

Promises enable modern syntax like async/await:

async function run() {
const data = await fetchData();
console.log(data);
}

This looks like synchronous code but works asynchronously.


Final Thoughts

  • Node.js uses async code to avoid blocking execution

  • Callbacks were the first solution but led to complex and unreadable code

  • Promises improved structure and made async code easier to manage

  • Today, async/await is the most commonly used approach, built on top of promises