Prefer reading? The full explanation
How queueMicrotask Runs
queueMicrotask vs Promise.then · Browser & Node.js
Before this
How Promises Run — the microtask vs task distinction.
After this you can explain
That queueMicrotask and Promise.then share one FIFO queue, why that makes it different from setTimeout(0), and what recursive microtasks starve.
console.log('1: sync');
setTimeout(() => console.log('5: setTimeout'), 0);
queueMicrotask(() => console.log('3: queueMicrotask'));
Promise.resolve().then(() => console.log('4: promise'));
console.log('2: sync end'); Output: 1: sync → 2: sync end → 3: queueMicrotask → 4: promise → 5: setTimeout
The door next to the promise
Before queueMicrotask existed, code that needed "run this after the current job, before anything else" had one idiom: Promise.resolve().then(fn). It worked because promise reactions are delivered through the microtask queue — but it allocated a promise nobody wanted, and it read like a trick.
queueMicrotask(fn) is that idiom with the trick removed. The HTML Standard specifies it as: enqueue fn on the same microtask queue promise reactions use. Not a similar queue — the same one. Which is why the case above is not really about priority at all: line 5 enqueues before line 7, so line 5's callback runs first. Swap the two lines and the output swaps with them.
queueMicrotask vs setTimeout(0): a checkpoint is not a turn
The tempting mental model is that queueMicrotask is just setTimeout(fn, 0) minus the delay. But the two differ in when the host is allowed to do other things. A timer callback is a task: before the loop gets to it, it may finish other queued tasks and — in a browser — update the rendering. A microtask runs at the checkpoint inside the current turn, after the running script and before any of that.
That difference is visible: state deferred to a timer can flash on screen one frame late, because a paint slipped in between; state deferred to a microtask cannot, because the checkpoint precedes the rendering step. The rendering case on this site shows exactly where that boundary sits.
In Node the comparison is even less flattering for the timer: setTimeout(fn, 0) is clamped to 1ms and lands in the timers phase, with ordering consequences the setImmediate case demonstrates. queueMicrotask behaves identically in both hosts — it is the portable one.
What the greedy drain buys — and costs
The microtask checkpoint drains the queue completely, including microtasks enqueued while draining. That rule is what makes promise chains feel atomic: every link that can run, runs, before the world moves on.
The experiment turns the same rule against you. Each microtask queues the next; the queue refills mid-drain; and a 0ms timer — expired, eligible, ready — waits until the whole family line has finished. Cap the recursion at three and the timer is merely late. Remove the cap and the timer never runs: no task, no I/O, no paint, ever. The loop is not broken; it is doing exactly what the checkpoint rule says.
This is the same starvation the Node case shows with recursive process.nextTick, one queue up. The lesson is identical: microtasks are for finishing the current thought, not for chunking long work. For that, yield to a real task — setImmediate in Node, or a timer/postMessage in the browser.
When it is the right tool
Reach for queueMicrotask when work must happen after the current synchronous run but before the host does anything else: emitting an event after the constructor returns so listeners can attach, deduplicating a burst of synchronous calls into one flush, or making an API that is sometimes-sync behave consistently async without paying for a promise.
The test is the phrase "before anything else". If the deferred work could happily wait a turn — analytics, cleanup, non-urgent state — a task is kinder to the page: it lets rendering and input in first. The microtask queue is a cut-in-line mechanism; use it when cutting in line is the point.
Reading about it is one thing. Watching it run is another.
↑ Run it step by step