NodeJSMicrotasks vs Macrotasks

Microtasks vs Macrotasks

Every asynchronous callback in Node lands in one of two categories: a microtask or a macrotask. The difference governs when it runs relative to everything else — and getting it wrong leads to bugs where code runs in a baffling order. This page pins down the distinction precisely.

Which is which

Microtasks (high priority)

Macrotasks (one per phase)

Promise.then / catch / finally

setTimeout / setInterval

await continuations

setImmediate

queueMicrotask(fn)

I/O callbacks (fs, net, …)

process.nextTick*

'close' events

*nextTick is even higher than microtasks
`process.nextTick` technically has its *own* queue that drains *before* the Promise microtask queue. So the true priority is: **nextTick → promises → macrotasks**. For most reasoning you can lump nextTick in with microtasks ("runs very soon"), but when both are queued, nextTick wins.
The golden rule of ordering
ALL microtasks drain before the NEXT macrotask
After the current synchronous code (or any single macrotask) finishes, the engine empties the **entire** microtask queue — including microtasks added *by* other microtasks — before it runs even one more macrotask. Microtasks can starve macrotasks: an endless chain of `.then` that keeps queueing more microtasks will prevent any timer or I/O callback from ever running.

The drain cycle

Text
run one macrotask (or the initial sync script)
        │
        ▼
drain microtask queue COMPLETELY   ← incl. microtasks queued during the drain
        │
        ▼
run the NEXT single macrotask
        │
        ▼
drain microtask queue COMPLETELY  …and so on
Seeing it in action

JS
console.log('1: sync')

setTimeout(() => console.log('2: macrotask'), 0)

Promise.resolve()
  .then(() => console.log('3: microtask'))
  .then(() => console.log('4: microtask chained'))

console.log('5: sync')
1: sync
5: sync
3: microtask
4: microtask chained
2: macrotask

Both sync logs run, then the whole promise chain (3 then 4) drains as microtasks, and only then the setTimeout macrotask (2). The chained .then (4) jumps ahead of the timer even though it was queued later — because it's a microtask.

Microtask starvation
A recursive microtask freezes the loop
Because microtasks drain *completely* before any macrotask, a microtask that always queues another microtask never lets the loop advance — timers never fire, I/O never completes, the process hangs at 100% CPU. The same recursion via `setTimeout` or `setImmediate` (macrotasks) is safe, because each lets the loop breathe between iterations.

JS
// ✗ STARVATION — this timer NEVER fires
function loop() { Promise.resolve().then(loop) }
loop()
setTimeout(() => console.log('I never run'), 0)

// ✓ SAFE — yields to the loop each iteration
function safeLoop() { setImmediate(safeLoop) }
Why this matters in practice
  • Code after await is a microtask — it resumes before any pending setTimeout, which surprises people timing things.

  • Need to yield to I/O / let the loop breathe? Use a macrotask (setImmediate), not a promise.

  • Need to defer until "right after this code, before anything else"? Use a microtask (queueMicrotask) or process.nextTick.

  • Don't build unbounded microtask recursion — break it up with setImmediate so timers and I/O still run.

queueMicrotask vs nextTick vs setImmediate

API

Queue

Runs

process.nextTick(fn)

nextTick (highest)

Before promises, before next phase

queueMicrotask(fn)

Microtask

With promise reactions

setImmediate(fn)

Macrotask (check phase)

After current poll phase

setTimeout(fn, 0)

Macrotask (timers phase)

Next tick, ≥1ms later

The two micro-level schedulers — nextTick and queueMicrotask — and their relationship to setImmediate are explored further in setImmediate & process.nextTick.

Next
The flip side of all this scheduling — what happens when you *don't* yield: [Blocking vs Non-Blocking Code](/nodejs/blocking-vs-nonblocking).