Prefer reading? The full explanation
How Promises Run
Promise vs setTimeout · Browser
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.
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