Promise vs setTimeout
Predict the output first. Then watch the queues prove you right, or, more usefully, wrong.
- Level
- DEVELOPER
- Read time
- 06 min
- Experiment
- Available
- Type
- THOUGHT
The question
Given nested timeouts, promise chains and an async function, in what order does the output appear?
Hypothesis
Every microtask queued during the current turn runs before the next timer callback, no matter when the timer was scheduled.
Method
Laboratory available
The instrument for this investigation runs in the laboratory, where the controls, the live model and the observation log share one workstation.
Enter the laboratoryconsole.log(1);setTimeout(() => { console.log(2); Promise.resolve().then(() => console.log(3));});Promise.resolve().then(() => { console.log(4); setTimeout(() => console.log(5));});(async () => { console.log(6); await null; console.log(7);})();console.log(8);What we observed
168472356 is synchronous, the body of an async function runs immediately up to the first await. 8 finishes the synchronous phase. Then the microtask checkpoint drains in queue order: 4 was queued before 7. Only then does the first timer run (2), and its own microtask (3) is drained before the second timer (5).
Why it happens
await null still yields. The value is wrapped with Promise.resolve(), so resumption is scheduled as a microtask rather than continuing inline. This is why a loop of awaits over already-resolved values is not free: each iteration costs a microtask checkpoint round trip.
// Two microtask hops (await + .then)await fetchUser();render();// One: the continuation is inside the handlerfetchUser().then(render);Further research
The ordering above was standardised late. Prior to 2018 several engines resolved await with an extra two microtask ticks, so the same program printed a different order in Node 8 than in Node 12. If you find an old blog post that disagrees with the bench, it is probably describing the pre-optimisation semantics.
References
- 01V8 blogawait takes 2 ticks less
- 02MDNasync function