"What is the output" questions about promises and timers are the JavaScript interview's litmus test. They are not trivia: the same model explains why Vue batches updates into a microtask, why a for loop of awaits is slow, and why a 300 ms click handler ruins INP.
queueMicrotask, MutationObserver), including microtasks queued while draining. Then, in browsers, a rendering opportunity may occur (style, layout, paint; requestAnimationFrame callbacks run just before). Then one task (a timer, an I/O callback, an event) is taken from the task queue and the cycle repeats.await is a microtask boundary. await x suspends the function and resumes in a microtask after x settles; awaiting a native promise costs one microtask tick, awaiting a non-promise value also yields once. Code after await never runs synchronously with the code before it.setTimeout(fn, 0) runs after at least one full turn, browsers clamp nested timers to 4 ms after a few levels and throttle background tabs; setInterval drifts. Never use timers to "wait for the DOM" when await nextTick() or requestAnimationFrame expresses the intent.await scheduler.yield() (yields and resumes with priority; Baseline in 2025 browsers) or setTimeout chunks, move CPU work to a Web Worker (structuredClone transfer, Transferable buffers), and keep handlers short.unhandledrejection in browsers; in Node they crash the process by default. Every promise chain needs a consumer of its rejection; void promise is a lie to the linter, not a handler.AbortController + signal passed to fetch, addEventListener, streams and your own async functions; AbortSignal.timeout(ms) and AbortSignal.any([...]) compose signals; cleanup checks signal.aborted or listens for abort.Promise.all (fail fast), allSettled (wait for all), race (first settled), any (first fulfilled, AggregateError if none), Promise.withResolvers() (ES2024, replaces the deferred pattern), Promise.try(fn) (ES2025, runs sync-or-async functions uniformly with errors as rejections).for (const u of urls) await fetch(u) serialises; await Promise.all(urls.map(fetch)) runs concurrently; a concurrency limit (a pool of N) is the production version of both.setImmediate) → close) and process.nextTick, which runs before promise microtasks. setImmediate runs after I/O in the check phase; setTimeout(fn, 0) versus setImmediate ordering is only deterministic inside an I/O callback.Predict, then run:
setTimeout(() => console.log('A timeout'), 0)
requestAnimationFrame?.(() => console.log('B rAF (browser, before next paint)'))
Promise.resolve().then(() => {
console.log('C microtask')
queueMicrotask(() => console.log('D nested microtask, still before any task'))
})
;(async () => {
console.log('E sync part of async fn')
await undefined
console.log('F after await')
})()
console.log('G sync')
// Browser: E, G, C, F, D, then B (if a frame is due) and A. Node: E, G, C, F, D, A (no rAF).
A concurrency-limited runner with cancellation, the shape interviewers ask you to write:
export async function mapWithLimit<T, R>(
items: readonly T[],
limit: number,
fn: (item: T, signal: AbortSignal) => Promise<R>,
signal?: AbortSignal,
): Promise<R[]> {
const results: R[] = Array.from({ length: items.length })
let next = 0
async function worker() {
while (next < items.length) {
signal?.throwIfAborted()
const index = next++
results[index] = await fn(items[index]!, signal ?? new AbortController().signal)
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker))
return results
}
// usage: await mapWithLimit(ids, 4, (id, signal) => $fetch(`/api/items/${id}`, { signal }), AbortSignal.timeout(10_000))
Breaking a long task so input stays responsive:
export async function processInChunks<T>(items: T[], handle: (item: T) => void, budgetMs = 8) {
let deadline = performance.now() + budgetMs
for (const item of items) {
handle(item)
if (performance.now() > deadline) {
// Yield to the event loop so clicks and paints can happen; resume with priority where supported.
await ('scheduler' in globalThis && 'yield' in scheduler ? scheduler.yield() : new Promise(r => setTimeout(r, 0)))
deadline = performance.now() + budgetMs
}
}
}
Retry with exponential backoff and jitter, honouring cancellation:
export async function retry<T>(fn: (signal?: AbortSignal) => Promise<T>, { retries = 3, baseMs = 200, signal }: { retries?: number, baseMs?: number, signal?: AbortSignal } = {}): Promise<T> {
for (let attempt = 0; ; attempt++) {
try {
return await fn(signal)
}
catch (error) {
if (attempt >= retries || signal?.aborted) throw error
const delay = baseMs * 2 ** attempt * (0.5 + Math.random()) // jitter spreads retries out
await new Promise((resolve, reject) => {
const t = setTimeout(resolve, delay)
signal?.addEventListener('abort', () => { clearTimeout(t); reject(signal.reason) }, { once: true })
})
}
}
}
for (const id of ids) { results.push(await load(id)) } takes the sum of the latencies. Unless order or rate limiting demands it, start the work first (ids.map(load)) and await the group, or use a limited pool. Interviewers ask this as "make it faster".
An async function called without await or .catch produces a promise nobody observes; when it rejects, Node exits and the browser logs an error you never see in tests. Lint for it (@typescript-eslint/no-floating-promises) and handle or explicitly void with a comment explaining why the result is ignored.
Browser support for scheduler.yield() and scheduler.postTask() as of the interview week; both were becoming Baseline through 2025.
setTimeout, Promise.then, await, queueMicrotask and (in Node) process.nextTick/setImmediate; predict, run, explain each discrepancy.mapWithLimit and test it with a fake 100 ms task: 20 items at limit 4 should take roughly 500 ms, not 2 s or 100 ms."JavaScript runs one task at a time on a single call stack. When the stack empties, the engine drains the microtask queue completely, promise reactions and queueMicrotask, including ones queued while draining, then the browser gets a chance to render, then it takes the next task: a timer, an event, an I/O callback. await is a microtask boundary: the function suspends and resumes in a microtask after the awaited promise settles, so the code after an await always runs later than the code around the call, but before any timer. That model explains everything else: Vue batching updates into a microtask, why sequential awaits are slow, why a long synchronous handler blocks input, and why setTimeout(fn, 0) is a full turn later."
"Start the work concurrently rather than awaiting in a loop, but bound the concurrency: a pool of N workers pulling from a shared index, Promise.all over the workers, results written by index so order is preserved. Every task receives an AbortSignal so the whole batch can be cancelled with one AbortController or an AbortSignal.timeout, and transient failures retry with exponential backoff plus jitter so retries do not synchronise. That is p-limit and p-retry in thirty lines, and knowing the shape means I can also explain why a naive Promise.all over a thousand fetches is a self-inflicted denial of service."
Nothing yet. Notes filed here show up on this page and the section overview.
JavaScript & the runtime
Why senior frontend interviews still test JavaScript itself — the event loop, closures and this, modules, collections and iterators, memory, dates and numbers, and what ES2024 to ES2026 added — and how the pages in this section fit.
Closures, scope, this and prototypes
Lexical scope and the temporal dead zone, what a closure captures and what it keeps alive, the four rules for this, arrow functions and class fields, prototypes underneath classes, private fields, descriptors, Proxy and Reflect, and equality.