Case 5/15

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

In the wild

Where this rule bites real code

The deferred update that flickers

Symptom
State is updated in a setTimeout(fn, 0) to "run it after the current code". Users see a one-frame flash of the old state before the update lands.
Why
A task is on the other side of a potential rendering step: the browser may paint between the current task and your timer callback. A microtask runs at the checkpoint BEFORE rendering — no frame can sneak in.
Fix
Defer with queueMicrotask when the work must complete before the next paint. Keep setTimeout for work that should genuinely wait a turn (or longer).

The event emitted before anyone could listen — portable edition

Symptom
A class emits an event in its constructor; listeners attached on the next line miss it. The Node fix (process.nextTick) breaks in the browser build.
Why
Emitting synchronously fires before the caller's next line. process.nextTick fixes it but exists only in Node.
Fix
queueMicrotask(() => this.emit("ready")) — identical "after this turn, before anything else" timing, specified for every JavaScript host.

Environment

Browser & Node.js

CI verified

Node.js v22

queueMicrotask is specified identically for browsers and Node (WHATWG HTML); the ordering rules validated here are environment-independent

Last reviewed

4 August 2026

The animation on this page is a teaching model, not a diagram of browser internals. Real engines use several task queues, optimise aggressively, and do plenty of work these panels do not show. What is guaranteed is the observable ordering: the stated output is produced by actually executing the code in CI, on the runtime named above. Browser-specific behaviour follows the HTML Standard and is not executed in a real browser by CI. How this is verified →