Case 3/15

Prefer reading? The full explanation

How Promises Run

Promise vs setTimeout · Browser

Before this

How the Event Loop Runs — synchronous code and the Task Queue.

After this you can explain

Why a promise callback registered later still runs before a timer callback registered earlier.

console.log('A');

setTimeout(() => console.log('B'), 0);

Promise.resolve().then(() => console.log('C'));

console.log('D');

Output: A → D → C → B

Why does C print before B?

The output is A → D → C → B. Most wrong answers pick A → D → B → C, on the reasoning that line 3 registered its callback before line 5 did. That reasoning fails because the two callbacks never sit in the same queue, so their registration order never gets compared.

setTimeout hands its callback to the browser. When the timer expires, the callback lands in the task queue. A resolved promise puts its .then callback somewhere else entirely: the microtask queue, which is also where queueMicrotask and MutationObserver callbacks go. Same script, two different waiting rooms.

Microtask queue vs task queue

The event loop checks the two queues in a fixed order. Once the call stack is empty, it drains the microtask queue completely, takes one task from the task queue, then checks microtasks again. So a promise callback queued on the last line of a script still runs before a timer callback queued on the first line.

This ordering is not a browser quirk you happen to observe. It comes from the event loop processing model in the HTML spec, and Node resolves promises ahead of timers the same way, so code can rely on it.

Line by line

The script prints A, starts the timer, queues C as a microtask, prints D, and exits. Console so far: A D, with one callback waiting in each queue.

The stack is now empty. Microtasks drain first, so C prints. The task queue finally gets its turn and B prints last.

A note on the word "macrotask"

The HTML standard has no such term. It defines tasks and microtasks, and a browser keeps several task queues — one for timers, one for user interaction, and others — which it is free to choose between. "Macrotask" is a community shorthand for "an ordinary task", widely used and worth knowing, but you will not find it in the spec.

The precise version of the rule on this page is therefore: once the current task finishes and the call stack is empty, every queued microtask runs before the event loop selects the next task. That wording also avoids a second misreading — microtasks do not preempt anything. A promise callback cannot interrupt a timer callback that is already running.

Where this shows up in real code

If microtasks keep queueing more microtasks, the loop never reaches the task queue: timers stall and the page stops painting. That is a real bug class, not just an interview trick. The same priority is the reason UI frameworks flush state updates in microtasks, since those are guaranteed to run before the next paint and before any timer.

The two experiments above are worth actually running. Swap setTimeout for queueMicrotask and B beats C again, because callbacks in the same queue run in FIFO order. Chain a second .then and E still jumps ahead of B, because the drain continues until the microtask queue is empty.

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 "flushed" update that reads stale state

Symptom
You set a value, schedule work with setTimeout(fn, 0) to run "right after", and fn sometimes sees state changed by other code you did not expect to run first.
Why
Every pending microtask — promise callbacks, awaits resuming — runs before your 0ms timer. "Right after" via a timer is actually "after everything else".
Fix
If the work must run before anything else queued, use queueMicrotask. If it must run after paint, use requestAnimationFrame. Reserve setTimeout(fn, 0) for genuinely yielding to the loop.

A loading spinner that never appears

Symptom
You show a spinner, then run a chain of promise-based work. The spinner never renders — the page just freezes until everything finishes.
Why
The microtask queue drains completely before the browser may render. A long promise chain that never touches a task boundary keeps paint waiting indefinitely.
Fix
Break the chain with a task boundary: await new Promise(r => setTimeout(r, 0)) between heavy stages, or move the work to a Web Worker.

Environment

Browser

CI verified

Node.js v22.22.3

Last reviewed

29 July 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 →