Async Execution Ordering

Hard100% Free~30 mins#async-await#promises#event-loop#microtasks#macrotasks#output-prediction
Key Learning Objectives
✓

Trace asynchronous interleaving across synchronous Call Stack execution, Microtask Queue, and Task Queue.

✓

Understand the synchronous execution of Promise constructor executors.

✓

Master async/await desugaring: synchronous execution up to the first await expression, followed by microtask resumption.

✓

Analyze chained Promise .then() callbacks and understand how promise resolution queues subsequent microtasks in turn.

✓

Master event loop queue draining order: all microtasks drain completely before the next macrotask (setTimeout) executes.

The Interview Problem

What is logged to the console when the following code executes, and how do async/await expressions interleave with chained Promise.then callbacks and timer macrotasks?

1const log = [];
2
3log.push('1');
4
5setTimeout(() => {
6 log.push('2');
7 console.log(log.join(' '));
8}, 0);
9
10async function asyncFn() {
11 log.push('3');
12 await null;
13 log.push('4');
14}
15
16asyncFn();
17
18new Promise((resolve) => {
19 log.push('5');
20 resolve();
21})
22 .then(() => {
23 log.push('6');
24 })
25 .then(() => {
26 log.push('7');
27 });
28
29log.push('8');
Predict Console Output
Interactive Challenge

Select the option that matches what standard ECMAScript prints to the console:

1 3 5 8 4 6 7 2

1 8 3 5 4 6 7 2

1 3 5 8 2 4 6 7

1 3 5 8 6 7 4 2

V8 Engine Execution Trace
Step 1 of 11 (Line 3)

Pushes '1' synchronously.

Call Stack (Top = Active)
Global Execution Context
Lexical Scope / Bindings
log:['1']
Console Stream
> [empty]

Deep Technical Breakdown

The Step-by-Step V8 Execution Model

To master async execution order questions in interviews, follow this deterministic 3-tier mental model:

Tier 1: The Synchronous Execution Phase

Code on the Call Stack executes immediately from top to bottom:

  1. log.push('1') runs.
  2. setTimeout(..., 0) registers a timer with the browser/Node host environment, placing its callback into the Macrotask Queue.
  3. asyncFn() is invoked. Contrary to common belief, calling an async function is synchronous until the first await! log.push('3') executes immediately.
  4. When await null is reached, JavaScript wraps the operand in a Promise (Promise.resolve(null)), attaches the remainder of asyncFn as a fulfillment handler, and returns control to the caller. This enqueues Microtask #1 (resume asyncFn).
  5. new Promise(executor) is evaluated. The executor function executes synchronously immediately upon creation. log.push('5') runs, followed by resolve().
  6. The .then(() => log.push('6')) attaches to the already-resolved promise, enqueuing Microtask #2.
  7. log.push('8') runs synchronously.

At the end of Tier 1, the console state is [1, 3, 5, 8].

Tier 2: The Microtask Draining Phase

Whenever the Call Stack empties, the engine must exhaustively drain the Microtask Queue before touching any macrotasks:

  1. Microtask #1: Resumes asyncFn(). Pushes '4'. Queue now has [Microtask #2].
  2. Microtask #2: Executes the first .then(). Pushes '6'. Its completion resolves the promise returned by the first .then(), which schedules Microtask #3 (the chained .then()).
  3. Microtask #3: Executes the second .then(). Pushes '7'.

The Microtask Queue is now completely empty.

Tier 3: The Macrotask Phase

The event loop selects the oldest task in the Macrotask Queue:

  1. The setTimeout callback runs. Pushes '2'.
  2. console.log(log.join(' ')) logs '1 3 5 8 4 6 7 2'.
Common Traps & Mistakes

Believing `async` functions defer their entire execution to a microtask. Only the code *after* the `await` keyword is deferred.

Thinking `new Promise((resolve) => { ... })` runs asynchronously. The executor runs synchronously on the active call stack frame.

Assuming `setTimeout(fn, 0)` can execute before microtasks. Macrotasks are strictly held until the microtask queue is 100% empty.

Failing to understand that chained `.then()` callbacks cannot be enqueued until the preceding `.then()` callback finishes executing.

FAANG Follow-Up Probes
Probe #1

How does `queueMicrotask` differ in execution priority compared to `Promise.resolve().then()`?

Probe #2

What would happen if a microtask continuously enqueued more microtasks in an infinite loop? (Event loop starvation: UI freezes and timers never execute).

Probe #3

How does the browser rendering step (style recalculation, layout, paint) interleave between microtasks and macrotasks?