Async and event loops — every question, written out
One thread, three queues, and exactly one valid schedule. What await suspends, what a blocking call costs everyone else, and what async does not make parallel.
A `setTimeout(fn, 0)` is registered, then a `Promise.resolve().then(g)`. Which runs first?
Trace prediction
`g` — microtasks drain completely before the loop takes a macrotask.
The loop finishes the current task, drains every microtask — including ones queued during the drain — and only then takes one macrotask. Promise continuations are microtasks; timers are macrotasks.
See every interleaving — One schedule: sync, then promise, then timer.
How many valid orderings does an event-loop program have?
Invariant identification
Exactly one — the queue discipline admits no choice at any point.
Run to completion, drain microtasks fully, take one macrotask. Each rule admits at most one action, so the whole outcome space has a single member — which is the guarantee that removes data races.
Can single-threaded JavaScript still have a race condition?
Edge case reasoning
Yes — state read before an `await` can be stale after it, so two invocations can interleave.
There are no data races, and check-then-act is alive and well with `await` as the window. The advantage is that the interleaving points are visible — they are exactly the `await`s.
One handler does 500ms of synchronous CPU work. What happens to concurrent requests?
Code diagnosis
They all wait 500ms — there is no preemption to take the CPU back.
Run-to-completion is the guarantee that removes races and the property that makes this unavoidable. Every ready callback queues behind your function for its full duration.
See every interleaving — Two ready requests stalled behind one call.
Does marking the blocking function `async` fix it?
Code diagnosis
No — `async` changes the return type, not where the CPU work runs.
The work must move off the loop — a worker thread or another service — or be chunked with yields between the pieces. Decorating it does nothing at all.
How do you detect that something is blocking the loop?
Code diagnosis
Measure event loop lag — how late a scheduled callback actually fires.
Loop lag isolates the loop from the work. `monitorEventLoopDelay` gives a histogram; a p99 in the tens of milliseconds means something is blocking, and that metric finds it when latency dashboards cannot.
Why is `Promise.all([a(), b()])` faster than awaiting `a()` then `b()`?
Comparison
Both requests are in flight before either is awaited, so the waiting overlaps.
Sequential `await` suspends the function that would have sent the second request, so it has not been sent. The total goes from the sum of the latencies to the maximum.
See every interleaving — overlapped == 1 against overlapped == 0.
One input to `Promise.all` rejects. What happens to the others?
Edge case reasoning
They keep running uncancelled; their results are discarded.
`Promise.all` rejects on the first rejection and cancels nothing. A later rejection from an abandoned promise becomes an unhandled rejection, which terminates the process in modern Node.
When would you use `Promise.allSettled` instead of `Promise.all`?
Comparison
When partial success is useful and you want every outcome reported separately.
It waits for everything and returns a status per input, which is what you want when three of four services answering is a usable result.
Is `for (const id of ids) { await fetch(id) }` a bug?
Code diagnosis
Only if the iterations are independent — then it costs the sum instead of the maximum.
The most common performance bug in async code, and it has a legitimate use. Independent iterations should be `Promise.all` over a map — with the fan-out bounded if the list is large.
Why bound the concurrency of a `Promise.all` over ten thousand items?
Trade-off & selection
Otherwise you open ten thousand simultaneous requests against a dependency with finite capacity.
Unbounded fan-out is a load amplifier pointed at your own dependencies, and it puts them on the wrong part of the utilisation curve. A semaphore or a limiter caps it.
A function starts background work and returns without awaiting it. What is wrong?
Code diagnosis
Its errors have nowhere to go, its lifetime is unbounded, and shutdown kills it mid-flight.
The caller believes the operation finished because it returned. Structured concurrency makes lifetime a property of the code’s shape: whatever you start in a scope finishes or is cancelled before the scope closes.
See every interleaving — The child runs after the parent returned, in the only ordering there is.
When is fire-and-forget acceptable?
Trade-off & selection
When the task is owned by a longer-lived scope that will wait for it at shutdown.
Structured concurrency does not forbid long-lived background work. It requires the work to have an owner, so that it is subject to shutdown and its failures surface somewhere.
What is the difference between concurrency and parallelism?
Comparison
Concurrency is several things in progress; parallelism is several executing at the same instant.
One is a property of the program, the other of the machine. A single core can run eight waiting tasks concurrently and finish far sooner than doing them one at a time, with no parallelism involved.
See every interleaving — Drag the waiting share and watch which line moves.
A service is slow and CPU sits at 15%. What does it need?
Code diagnosis
More concurrency — it is waiting, and extra cores would idle too.
Low CPU with high latency means the time is spent waiting. Overlap the waits — async I/O, or a pool sized by cores times one plus the wait-to-compute ratio.
What happens if a microtask queues another microtask, forever?
Edge case reasoning
The macrotask queue is starved: timers never fire and nothing else runs.
Microtasks queued during the drain are drained in the same pass, so an infinite chain hangs the loop with no error reported. It is the microtask equivalent of an infinite loop, and it is a real way to freeze a tab.