Skip to main content

Command Palette

Search for a command to run...

Async Code in Node.js: Callbacks and Promises

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

When building applications in Node.js, many operations take time to complete.

Examples:

  • Reading files

  • Fetching data from APIs

  • Connecting to databases

  • Waiting for user input

If Node.js waited for each task to finish before moving forward, applications would become slow and inefficient.

This is why Node.js uses asynchronous code.

In this article, we’ll understand:

  • Why async code exists in Node.js

  • Callback-based asynchronous execution

  • Problems with nested callbacks

  • Promise-based async handling

  • Benefits of promises

Let’s begin with a real-world example.


Why Does Async Code Exist in Node.js?

Imagine reading a large file from your computer.

If Node.js stopped everything until the file finished loading, the entire application would freeze.

Instead, Node.js starts the file-reading task and continues doing other work.

When the file is ready, Node.js handles the result later.

This is called asynchronous programming.


Synchronous vs Asynchronous Code

Synchronous Code

console.log("Start");

console.log("Reading file...");

console.log("End");

Output:

Start
Reading file...
End

The code runs line by line in order.


Asynchronous Example

Node.js provides asynchronous functions like fs.readFile().

Example:

const fs = require("fs");

console.log("Start");

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

console.log("End");

Possible output:

Start
End
File content here

Notice:

  • Node.js does not wait for the file to finish reading

  • It continues executing the next lines

  • The callback runs later when the file is ready


What is a Callback?

A callback is a function passed into another function to run later.

Example:

function greet(name, callback) {
  console.log("Hello " + name);

  callback();
}

function sayBye() {
  console.log("Goodbye");
}

greet("Abhi", sayBye);

Output:

Hello Abhi
Goodbye

Here:

  • sayBye is passed as a callback

  • It executes after the greeting


Callback-Based Async Execution

Let’s understand async flow step by step.

Example:

const fs = require("fs");

console.log("1. Start Reading");

fs.readFile("demo.txt", "utf8", (err, data) => {
  console.log("2. File Content:", data);
});

console.log("3. Continue Execution");

Output:

1. Start Reading
3. Continue Execution
2. File Content: Hello World

How the Callback Flow Works

Start Program
      ↓
Start File Reading
      ↓
Continue Other Code
      ↓
File Reading Completes
      ↓
Callback Function Executes

This is the foundation of asynchronous programming in Node.js.


Problems with Nested Callbacks

Callbacks work well initially, but deeply nested callbacks can make code difficult to read.

Example:

loginUser(function(user) {
  getPosts(user, function(posts) {
    getComments(posts, function(comments) {
      console.log(comments);
    });
  });
});

This structure becomes hard to manage.

This problem is often called:

Callback Hell

Problems include:

  • Hard to read

  • Difficult to debug

  • Deep nesting

  • Poor maintainability


Introduction to Promises

Promises were introduced to make asynchronous code cleaner and easier to manage.

A Promise represents a value that may be available:

  • Now

  • Later

  • Or never


Creating a Promise

Example:

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

  if (success) {
    resolve("Operation Successful");
  } else {
    reject("Operation Failed");
  }
});

A promise has two possible outcomes:

  • resolve() → success

  • reject() → failure


Handling Promises

We use:

  • .then() for success

  • .catch() for errors

Example:

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

Output:

Operation Successful

Promise-Based Async Handling

Let’s rewrite a file-reading example using promises.

Example:

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

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

This looks cleaner compared to nested callbacks.


Callback vs Promise Readability

Callback Style

getUser(function(user) {
  getOrders(user, function(orders) {
    console.log(orders);
  });
});

Promise Style

getUser()
  .then((user) => getOrders(user))
  .then((orders) => console.log(orders))
  .catch((err) => console.log(err));

Promises reduce nesting and improve readability.


Benefits of Promises

Promises provide several advantages:

✅ Cleaner code

✅ Better readability

✅ Easier error handling

✅ Avoid callback hell

✅ Better async chaining

This is why modern Node.js applications heavily use promises.


Promise Lifecycle

A promise has three states.

Pending
   ↓
Resolved  OR  Rejected

States

State Meaning
Pending Operation still running
Resolved Operation successful
Rejected Operation failed

Practice Assignment

Try these tasks yourself.


1. Callback Example

Create a function that waits 2 seconds and then prints a message using a callback.

Example:

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

2. Promise Example

Create a promise that resolves after 2 seconds.

Example:

const promise = new Promise((resolve) => {
  setTimeout(() => {
    resolve("Data loaded");
  }, 2000);
});

3. Handle the Promise

Use .then() to print the resolved value.


And now, you know what Callbacks and Promises in Node.js are.

If you have any doubt or want to connect, feel free to drop a comment — I’d be happy to help.

Thanks for reading, and see you in the next blog!

Peace ✌️ and Happy Learning!