๐ 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?