🌀 Node.js Event Loop: Microtasks vs Macrotasks (Explained with Fun!)

If you’ve been working with Node.js and sometimes wonder “Why does my Promise run before my setTimeout?”, or “Why did this async callback execute earlier than I expected?”

it’s all thanks to the event loop and its two important VIP lists: macrotasks and microtasks.


Let’s break it down in a way your brain (and code) will thank you for.


📍 Step 1 — Meet the Event Loop


The event loop in Node.js is like a nightclub bouncer.

  • Macrotasks are the main guests — they get in one at a time, in the order they arrived.

  • Microtasks are the VIP guests — they get in before the next main guest, no matter how long the line of main guests is.


In Node.js, the loop works in phases, and each phase handles certain types of callbacks (timers, I/O, etc.).

Between every phase, the microtask queue gets to run — like sneaking in special guests before the next act.


🗂 Macrotasks vs Microtasks in Node.js


Macrotasks

 (a.k.a. “tasks”)

  • Scheduled to run in future turns of the event loop.

  • Examples in Node.js:

    • setTimeout

    • setInterval

    • setImmediate

    • Some I/O callbacks


Execution:

One macrotask runs → then we check microtasks → then we move to the next macrotask.


Microtasks

  • Run immediately after the current task finishes, before moving to the next macrotask.

  • Examples:

    • Promise.then(), .catch(), .finally()

    • process.nextTick() (Node.js only — even higher priority than Promises!)

    • queueMicrotask() (standard API)


Execution:

Finish the current macrotask → empty the microtask queue → next macrotask.


⏱ Execution Order Example


Let’s run this in Node.js:

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

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

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

console.log("Synchronous: start");

Expected Output:

Synchronous: start
Microtask: process.nextTick
Microtask: Promise.then
Macrotask: setTimeout

Why?

  1. Synchronous code runs first.

  2. process.nextTick has the highest priority in Node.js — it runs before other microtasks.

  3. Promise.then runs after all process.nextTick callbacks are done.

  4. Finally, the setTimeout callback runs (macrotask).


🏃‍♂️ Real-World Use Case: Splitting Heavy Work


If you do a CPU-heavy task in one go, Node.js will block the event loop, delaying everything — even incoming HTTP requests.


Bad Example:

function heavyTask() {
  for (let i = 0; i < 1e9; i++) {} // blocks everything!
  console.log("Done");
}

heavyTask();
console.log("This runs late");

Better: Break it up using macrotasks

let i = 0;
function chunkedTask() {
  for (let j = 0; j < 1e6; j++) {
    i++;
  }
  if (i < 1e9) {
    setTimeout(chunkedTask, 0); // let event loop breathe
  } else {
    console.log("Done");
  }
}
chunkedTask();

This way, Node.js can handle other events (like incoming requests) between chunks.


🛠 When to Use Each

Use case

Use

Why

Run after current code but before timers

process.nextTick

Critical cleanup or quick follow-up

Run after current macrotask but ASAP

Promise.then / queueMicrotask

Async follow-up without waiting for timers

Break heavy work into chunks

setTimeout / setImmediate

Give event loop a breather

Run after I/O

setImmediate

Runs right after the poll phase


📌 Summary

  • Macrotasks: Big steps in the loop (timers, I/O).

  • Microtasks: Small, urgent tasks that run before the next macrotask.

  • In Node.js:

    1. Synchronous code

    2. process.nextTick queue

    3. Promise microtasks

    4. Macrotask (e.g. setTimeout)


Final Thought 💡


If your async code runs earlier than you expected, it’s probably a microtask.

If it runs later, it’s probably a macrotask.

And if it runs way too late, you probably blocked the event loop. 🛑


If you want, I can also add an eye-catching ASCII diagram of the Node.js event loop phases with microtasks/macrotasks in it — that would make your blog post even more shareable.

Do you want me to add that?