Change theme

⚡ Asynchronous Programming in Node.js: Callbacks, Promises, and Async/Await

Published:
Reading time:
3 min.

One of the most powerful – and sometimes confusing – aspects of Node.js is asynchronous programming. It’s what makes Node.js blazing fast and non-blocking, but if you’ve ever defalt with callback hell, you’re not alone.

In this post, we’ll explore the three core ways to handle asynchronous code in Node.js:

  1. Callbacks
  2. Promises
  3. Async/Await

By the end, you’ll understand not just how to use them, but when and why to choose each.

🧠 Why Asynchronous Programming Matters

JavaScript in Node.s runs on a single thread. This means that if you perform a blocking tasks - like reading a big file or making an API request – your entire app halts until that task finishes.

Asynchronous programming lets us avoid that.

You can start a task, continue with other work, and handle the result later – without freezing your app.

1. 🧩 Callbacks: The Old-School Way

A callback is a function passed into another function to be called later - usually after async taks finishes.

Example:

const fs = require('fs');

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

Problem: Callback Hell 😵‍💫

If you have multiple nested callbacks, your code quickly becomes messy:

getUser(userId, (err, user) => {
  getPosts(user, (err, posts) => {
    getComments(posts, (err, comments) => {
      // 😨 deeply nested
    });
  });
});

2. 🔗 Promises: A Better Approach

A Promise represents a value that may be available now, or in the future, or never:

Example:

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

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

Benefits:

3. ⛱ Async/Await: Clean & Modern

Async/Await is syntatic sugar over Promises. It lets you write async code that looks like synchronous code – cleaner and easier to read.

Example:

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

async function readFile() {
  try {
    const data = await fs.readFile('file.txt', 'utf8');
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}

readFile();

Why it’s Awesome:

⚖️ When to Use What?

ApproachUse Case
CallbacksLegacy code or simple utilities
PromisesGood for chaining multiple async tasks
Async/AwaitPreferred for readability, error handling, and modern apps

🛠 Real-World Example: Fetching Data from an API

Let’s see how the same task looks in each style.

✅ Using Callback (with request library):

const request = require('request');

request('https://api.example.com', (err, res, body) => {
  if (err) return console.error(err);
  console.log(JSON.parse(body));
});

✅ Using Promise (with axios):

const axios = require('axios');

axios.get('https://api.example.com')
  .then(response => console.log(response.data))
  .catch(error => console.error(error));

✅ Using Async/Await:

const axios = require('axios');

async function fetchData() {
  try {
    const response = await axios.get('https://api.example.com');
    console.log(response.data);
  } catch (error) {
    console.error(error);
  }
}

fetchData();

💡 Tips for Working with Async Code

🚀 Final Thoughts

Understanding callbacks, promises, and async/await is crucial for every Node.js developer. If you’ve been stuck in callback hell, now’s the time to refactor your code and embrace the modern async flow.

Start using async/await today – your future self will thank you.


I try to keep my articles up to date, and of course I could be wrong, or there could be a better solution. If you see something that is not true (anymore), or something that should be mentioned, feel free to edit the article on GitHub.