Ten expert-tier output questions combining concurrent chains, resolve(promise) timing, async error layers, and multi-queue interleaving — the questions that define the senior JS interview. If you can get all ten right without peeking, your async mental model is complete.
Q1 — Breadth-first with concrete values
Loading editor...
Show answer
Output:
1
3
5
2
4
Why: Microtask queue initially: [cb1, cb3, cb5]. cb1 runs → 1 → schedules cb2. Queue: [cb3, cb5, cb2]. cb3 → 3 → schedules cb4. Queue: [cb5, cb2, cb4]. cb5 → 5. Then cb2 → 2. Then cb4 → 4. Breadth-first: all depth-1 callbacks (cb1, cb3, cb5) run before any depth-2 callback (cb2, cb4).
Q2 — resolve(promise) vs resolve(plain) head start
Loading editor...
Show answer
Output:
plain: plain
inner: inner
extra: extra
outer: inner
Why: resolve(inner) requires adoption — one extra microtask tick. Timeline:
- Sync ends. Microtask queue:
[adopt, plain-then, inner-then, extra-then] - Adoption runs →
outerresolves → queuesouter-then. Queue:[plain-then, inner-then, extra-then, outer-then] plain: plain,inner: inner,extra: extra,outer: inner.
outer's .then() is last because of the adoption overhead. All plain-value chains got a one-tick head start.
Q3 — return vs return await in a nested async chain
Loading editor...
Show answer
Output:
without failed: inner error
caught: inner error
with: recovered
Why: return inner() exits the try block immediately — inner() returns a rejected Promise (because inner is async — throw inside async creates a rejected Promise). The catch block never runs. withoutAwait() returns that rejected Promise, so the caller's .catch() fires.
return await inner() stays inside the try. await unwraps the rejection → inner error is thrown → catch intercepts, returns "recovered". The caller's .then() receives "with: recovered".
Q4 — Promise.race vs Promise.any on first rejection
Loading editor...
Show answer
Output:
sync
race: fast reject
any: slow resolve
Why: "sync" logs first. At ~50ms: fastReject settles. race immediately rejects (first settlement wins). At ~100ms: slowResolve fulfills. any skips rejections so fastReject is ignored — any fulfills with "slow resolve". This is the key difference: race = first to settle (resolve or reject); any = first to fulfill (skips rejections).
Q5 — Promise chain schedules setTimeout, setTimeout schedules Promise
Loading editor...
Show answer
Output:
sync
P1
P2
T2
P-in-T2
T1
P-in-T1
Why: Sync: "sync". Microtask drain: P1 → queues macrotask T1 → queues P2. Then P2 runs. Macrotask queue now: [T2, T1] (T2 was scheduled during sync, T1 during microtask). T2 → "T2" → queues microtask P-in-T2. Microtask drain: "P-in-T2". T1 → "T1" → queues microtask P-in-T1. Microtask drain: "P-in-T1". The key: macrotask scheduled inside a microtask goes to the end of the task queue, behind already-scheduled macrotasks.
Q6 — Async error propagation through three layers
Loading editor...
Show answer
Output:
A
B
C
sync
A caught: from C
Why: All pre-await code runs synchronously: "A", "B", "C". Then "sync". C() returns a rejected Promise. B()'s await re-throws it, making B() return a rejected Promise. A()'s await re-throws it, landing in A's catch. "after B" is unreachable. The error propagates up through each await like a synchronous throw through a call stack.
Q7 — Promise.allSettled output shape
Loading editor...
Show answer
Output:
3
fulfilled → ok
rejected → fail
fulfilled → delayed
Why: Promise.allSettled waits for all Promises (including the 50ms delay) and returns an array in input order. Fulfilled: {status:"fulfilled", value:...}. Rejected: {status:"rejected", reason:...}. All three are represented — nothing is lost. The 50ms delay means the whole thing takes ~50ms, but the structure doesn't change. allSettled never rejects.
Q8 — await inside .map() finishes before mapped promises
Loading editor...
Show answer
Output:
p is array of: Promises
sync
results: [10, 20, 30]
Why: .map(async fn) returns an array of Promises — "P" logs "Promises". main() suspends at await Promise.all(p) — the continuations (printing results) are microtasks. "sync" logs. After ~300ms (the longest timer), all Promises resolve: [10, 20, 30]. Note: even though n=1 resolves first at 100ms, the array order is preserved by Promise.all.
Q9 — for await...of sequential timing
Loading editor...
Show answer
Output (approx):
a 100 ms
b 100 ms
c 150 ms
Why: All three Promises are started at the same time (inside the promises array). for await...of awaits them in order: first promises[0] (~100ms) → "a" at ~100ms. Then promises[1] — already resolved at 50ms, so it yields immediately: "b" at ~100ms. Then promises[2] (~150ms) → "c" at ~150ms. Total time is the longest Promise (~150ms), not the sum of delays. Each iteration blocks on the current element's resolution.
Q10 — Everything at once
Loading editor...
Show answer
Output:
f1-start
f2-start
DONE
QM
f1-mid
f2 f2-error
all-ok all-ok2
f1-end
T
P-in-T
Why: Full tick-by-tick:
Sync: "f1-start", "f2-start", "DONE". f2() throws: its returned Promise is immediately rejected (.catch() will fire as microtask). f1() suspends at first await null. Promise.all is all-resolved → .then() queued as microtask. queueMicrotask queues QM. Macrotask: T.
Microtask queue after sync: [f1-mid, f2-catch, all-then, QM]
f1-mid→"f1-mid"→ secondawait→ schedulesf1-endat endf2-catch→"f2 f2-error"(the catch fires —f2returned a rejected Promise)all-then→"all-ok all-ok2"QM→"QM"- Now:
[f1-end]→"f1-end"
Macrotask: "T" → queues microtask P-in-T → microtask drain: "P-in-T".
Key Rules
| Pattern | Rule |
|---|---|
| Breadth-first interleaving | Multiple .then() chains alternate by depth: depth-1 all run before any depth-2 |
resolve(promise) extra tick | Adoption = 1 extra microtask — chains using resolve(plain) get a head start |
return fn() vs return await fn() | return exits try immediately; return await keeps the error inside |
Promise.race vs Promise.any | race = first settled; any = first fulfilled (skips rejections) |
setTimeout from .then() | Macrotask is queued at the end — runs after previously scheduled macrotasks |
Error through await layers | Propagates like sync call stack — each await re-throws |
Promise.allSettled | Never rejects; {status,value} or {status,reason} per entry |
[].map(async fn) | Returns array of Promises — wrap with Promise.all to await |
for await...of | Awaits in order; all Promises start immediately; total time = longest |
Go Deeper
- Output Quiz #13 — Mixed Async Topics (Easy) — the easy sampler
- Output Quiz #12 — Microtask Queue: Breadth-First Ordering — breadth-first deep dive
- Output Quiz #9 — async/await Patterns & return vs return await — the return vs return await trap
- Output Quiz #8 — Promise Combinators — race vs any in detail
- Output Quiz #3 — The Event Loop & Task Ordering — the microtask queue model
