Case 9/13

Prefer reading? The full explanation

How It All Runs Together

The full interview question · Browser

Before this

How await Runs — continuations as microtasks.

After this you can explain

A three-pass method that solves any sync + timer + promise + async ordering question.

console.log('1');

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

async function f() {
  console.log('3');
  await null;
  console.log('4');
}
f();

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

console.log('6');

Output: 1 → 3 → 6 → 4 → 5 → 2

The answer, and how to get there in three passes

The output is 1 → 3 → 6 → 4 → 5 → 2. Rather than tracing fourteen lines at once, sort every console.log into three buckets: what runs synchronously, what becomes a microtask, and what becomes a task.

Synchronous pass: 1 prints; the timer is registered; f() is called and its body runs immediately, printing 3, until await null suspends it; the .then callback is registered; 6 prints. Console so far: 1 3 6.

Microtask pass: two are waiting. f()'s continuation was queued on line 7, the .then callback on line 12, so they run in that order and print 4 then 5. Task pass: the timer callback finally prints 2.

The two traps in this snippet

The first trap is line 10. Calling an async function is not scheduling it. The body executes right away on the current call stack, which is why 3 beats 6. Only the part after the await is deferred.

The second trap is await null. There is no promise here at all, so it looks like there is nothing to wait for. The engine still wraps the value, suspends the function, and queues the continuation, which is why 4 cannot print until the synchronous phase is over.

Why 4 comes before 5

Both are microtasks, so neither has priority over the other, and the queue is strictly FIFO. The only question is which was queued first. Line 7 executed before line 12, so f()'s continuation was already waiting when the .then callback arrived.

The experiment below moves the .then above the call to f(). Nothing else changes, and the output becomes 1 → 3 → 6 → 5 → 4 → 2. Position in the source decides the order between microtasks, because position decides which one gets queued first.

A method that survives harder versions

Interviewers extend this pattern by adding nested promises, chained .then() calls, or a second async function. The three-pass method still works: find the synchronous prints, list the microtasks in the order they get queued, and remember that microtasks queued during the drain join the same drain.

The only extra rule worth memorising is that each link of a promise chain costs one additional turn, which is what makes independent chains interleave.

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 "flaky" test that is actually deterministic

Symptom
A test asserts right after triggering async work and fails "randomly". The team adds a 50ms sleep and moves on, ashamed.
Why
The assertion runs in the synchronous phase; the code under test resolves across microtasks and tasks in exactly the three-pass order this case teaches. Nothing is random — the test just asserts one pass too early.
Fix
Await the actual promise under test. If a task boundary is genuinely involved, one await new Promise(r => setTimeout(r, 0)) flushes it — a full sleep is never the honest fix.

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 →