What does this script print, and why? async function inner() { console.log('inner'); } async function outer() { console.log('outer start'); await inner(); console.log('outer end'); } console.log('script start'); outer(); Promise.resolve().then(() => console.log('then')); console.log('script end');
answer
- calling it does not schedule it
- synchronous until the first await
- the rest of the body is queued right there
- queue order follows call order
basics
~20 sIt prints script start, outer start, inner, script end, outer end, then. An async function runs synchronously up to its first await; everything after that await is a microtask, queued at the moment the await is reached.
solid answer
~40 sOutput: `script start`, `outer start`, `inner`, `script end`, `outer end`, `then`. Calling `outer()` runs its body synchronously until the `await`, so `outer start` prints, then `inner()` runs to completion and prints `inner`. At the `await`, `outer` suspends and returns a pending promise to the caller, and the resumption — everything after the `await` — is queued as a microtask. Only then does the following line register the `.then` callback, which is queued second. `script end` prints as the script finishes; the microtask queue then drains in order, printing `outer end` before `then`. On a modern ES2019+ engine, awaiting a native promise costs exactly one microtask tick; older engines added two extra ticks and flipped the last two lines.
code
javascript · 11 linesasync function init() {
await Promise.resolve();
globalThis.ready = true;
}
init();
console.log(globalThis.ready); // undefined — the part after await has not run yet
queueMicrotask(() => {
console.log(globalThis.ready); // true — the resumption microtask ran first
});go deeper
Remember that an async function starts running immediately and only pauses at its first await, and that the code after an await runs later than the caller's next line. Be able to produce the six log lines in order.
Explain the mechanism: await suspends, returns a pending promise to the caller, and queues the remainder as a microtask at that moment — which is why queue order tracks call order rather than source position.
Bring the real bug: a fire-and-forget async call whose post-await effects have not happened when the next line reads them. Mention that tick counts differ for thenables and differed on pre-ES2019 engines, and state which baseline you are assuming.
Own the API contract. Decide whether functions may be safely called without awaiting, whether initialization should expose a readiness promise instead of a mutable flag, and how you keep ordering assumptions out of code that other teams maintain.
## The output ``` script start outer start inner script end outer end then ``` ## An async function is synchronous until its first await The most common wrong assumption is that calling an `async` function "schedules" it. It does not. `outer()` is an ordinary call: the body starts executing immediately, on the caller's stack. So `outer start` prints right where the call is, and the call to `inner()` also runs synchronously to completion — `inner` has no `await` at all, so its whole body runs and it prints `inner`, then returns a promise that is already fulfilled with `undefined`. An async function always returns a promise. `inner()`'s promise is already fulfilled; `outer()`'s is still pending, because `outer` has not finished. ## What await actually does Reaching `await x`: 1. Suspends the async function at that exact point, capturing where to resume. 2. Attaches the resumption as a reaction to (a promise for) `x`. 3. Returns a pending promise to the caller, so the caller keeps running its own code. The key trace consequence: **the code after the `await` is a microtask, and it is queued at the moment the `await` is reached** — not when the script ends. Because `outer()` is called before the `.then` line, the resumption is queued *first*, and FIFO order in the microtask queue puts `outer end` before `then`. ## How many ticks does an await cost? Since the ES2019 change adopted by all current engines, `await p` where `p` is a native already-fulfilled promise costs exactly **one** microtask tick: the engine skips wrapping it in a fresh promise and attaches the resumption directly. `await 42` — a non-promise value — also costs one tick, since the value is wrapped in an already-fulfilled promise and the reaction is queued. Before that change, awaiting a promise created two extra intermediate promises and cost three ticks, which is why older blog posts and older engines produce `then` before `outer end` for exactly this script. If an interviewer asks about the discrepancy, name the change rather than arguing that one output is wrong — both were correct for their engine. One case still costs extra ticks today: awaiting a **thenable** — a non-promise object with a `then` method, such as a hand-rolled or library-specific promise implementation. The engine has to call that `then` method from a job of its own, so the resumption lands one or more ticks later than the native case. That is a real source of interleaving surprises when mixing promise libraries with native code. ## Tracing it as columns | now (synchronous) | microtasks, in queue order | |---|---| | script start | | | outer start | | | inner | 1: resume outer -> `outer end` | | script end | 2: `then` | Read the left column, then the right column top to bottom. ## The trap that generalizes Rewrite the middle of the script as two calls: ```js outer(); console.log('after outer()'); ``` `after outer()` prints before `outer end`, every time. This is the practical lesson hiding inside the puzzle: calling an async function and ignoring its promise does **not** mean its later half has run. Code that calls `init()` without awaiting it and then reads the state `init` sets after its first `await` reads stale state — a bug that survives review precisely because the function "looks" sequential. ## What to say out loud Walk it in four beats: async functions start synchronously; the first `await` suspends and queues the rest; the caller continues; microtasks then run in the order they were queued. If you also mention that the count of ticks per `await` is version-sensitive and state the modern rule, you have answered above the level of the question.
- How does the trace change if outer() is awaited at top level instead of called bare?The statements after the top-level `await` are themselves deferred, so `then` would be registered only after `outer` completed, and the ordering question disappears — everything reads sequentially. That is the point of awaiting: you buy back source order at the cost of not running the following statements early.
- Why can awaiting a promise from a third-party library print later than awaiting a native one?A library object is usually a thenable — it has a `then` method but is not a native promise. The engine must call that `then` from a job of its own before the resumption can be queued, adding at least one microtask tick. Mixed native and library promises therefore interleave differently than either alone.
- Does `await 42`, with no promise involved, defer anything?Yes. The value is wrapped in an already-fulfilled promise and the resumption is queued as a microtask, costing one tick. So even an await of a plain value lets the caller's remaining synchronous code run first — the suspension is unconditional.
saying these in an interview costs you the question
- Says calling an async function schedules it for later
- Thinks nothing of the body runs until you await the result
- Claims the code after await runs as a timer callback
- Assumes await always suspends for the same number of ticks
- Reads calling init() as proof its post-await state is set