Change theme

🧠 Understanding Microtasks vs Macrotasks in Node.js (Made Simple)

Published:
Reading time:
5 min.

If you’ve been diving into the Node.js event loop, you might’ve heard terms like microtasks and macrotasks. There are two types of queues that Node.js uses to schedule asyncronous operations. But what exactly are they, and how do they affect your code?

In this blog, we’ll break down:

⏳ A Quick Refresher: The Event Loop

Node.js runs on a single-threaded, non-blocking event loop. When you run asynchronous code (like setTimeout or a Promise), it doesn’t block other operations - instead, it’s scheduled for later.
But when exactly it runs depends on whether it’s a microtask or a macrotask.

🔹 Microtasks: Tiny But Mighty

Microtasks are executed immediately after the currently executing script and before any macrotasks. These include:

These are high-priority tasks that run before the event loop continues to the next phase.

🔸 Macrotasks: Scheduled Tasks

Macrotasks (also called “tasks”) include:

These tasks are scheduled for the next iteration of the event loop.

🧪 Execution Order Example

Here’s a real-world example to see the difference:

console.log("Start");

setTimeout(() => {
  console.log("Macrotask: setTimeout");
}, 0);

Promise.resolve().then(() => {
  console.log("Microtask: Promise.then");
});

process.nextTick(() => {
  console.log("Microtask: process.nextTick");
});

console.log("End");

🔍 Output:

Start
End
Microtask: process.nextTick
Microtask: Promise.then
Macrotask: setTimeout

🧠 Explanation:

  1. Start and End are synchronous.
  2. process.nextTick() runs before any other microtasks (even Promises).
  3. Then Promise.then() runs.
  4. Finally, setTimeout() runs as a macrotask in the next tick of the event loop.

Execution Order Cheat Sheet

Task TypeExampleWhen It Runs
Synchronousconsole.log()Immediately
process.nextTickNode.js onlyBefore any other microtask
MicrotaskPromise.then()After current stack, before macrotasks
MacrotasksetTimeout()Next event loop cycle

⚠️ Common Pitfalls

1. Starvation by Microtasks

If you keep scheduling microtasks inside macrotasks, the event loop never gets to process macrotasks.

function infiniteMicrotasks() {
  Promise.resolve().then(infiniteMicrotasks);
}

infiniteMicrotasks(); // Freezes everything else

2. Assuming setTimeout(…, 0) runs immediately

It doesn’t! Microtasks still run first

✅ When Does This Matter?

🚀 Summary

📘 Bonus: Visual Flow

Synchronous Code
    ↓
process.nextTick Queue
    ↓
Microtask Queue (Promises)
    ↓
Macrotask Queue (Timers, I/O)Next Event Loop Tick ⟲

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.