🌀 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?
-
Synchronous code runs first.
-
process.nextTick has the highest priority in Node.js — it runs before other microtasks.
-
Promise.then runs after all process.nextTick callbacks are done.
-
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:
-
Synchronous code
-
process.nextTick queue
-
Promise microtasks
-
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?