Handbooks / JavaScript / Chapter 4

Event Loop and Async

7 pages · ~4 min✓ Reviewed

Builds on this and Prototypes. Next up: Modern JavaScript Features.

JavaScriptPage 1

Event Loop Mental Model

1. Stack and Web APIs

  • One call stack, one thread
  • A function runs to completion before anything else
  • Web APIs (timers, fetch, DOM events) wait outside the stack
  • When ready, they hand a callback to a queue

2. Two Queues

QueueHolds
Task (macrotask)setTimeout, setInterval, I/O, UI events
Microtaskpromise reactions, queueMicrotask, MutationObserver

3. The Loop Rule

  1. Run one task
  2. Drain all microtasks
  3. Render if needed
  4. Pick next task
  • Long synchronous loop holds the stack
  • Timers, clicks, rendering freeze: nothing can be dequeued
Remember

Microtasks drain completely before render or the next task.

JavaScriptPage 2

Execution Order and setTimeout

1. Order of a Script

  1. Sync codewhole stack
  2. Microtasksdrain all
  3. Timer taskone, then repeat
  • Microtasks queued by microtasks run in the same drain
  • An endless microtask chain starves timers and rendering

2. setTimeout Rules

  • setTimeout(fn, 0) is not immediate
  • It queues a task after the stack and all microtasks
  • Browsers clamp nested timers to about 4ms
  • Equal delays fire in the order they were scheduled
  • The delay is a minimum, not a guarantee

3. Classic Trace

javascript
console.log('A');
setTimeout(() => console.log('T'));
Promise.resolve().then(() => console.log('P'));
console.log('B');

sync, then microtask, then timer

output
A
B
P
T
Remember

Sync first, then all microtasks, then the next timer. P always beats T, even with a 0ms delay.

JavaScriptPage 3

Promises and async/await

1. States and then()

  • Three states: pending, fulfilled, rejected
  • A promise settles once; its value never changes
  • then(onOk, onErr) returns a new promise
Handler doesNew promise
Returns a valuefulfils with it
Returns a promiseadopts its outcome
Throwsrejects with the error

2. Microtasks and await

  • then/catch/finally callbacks always run as microtasks
  • Even when the promise is already settled
  • An async function always returns a promise
  • await pauses only that function
  • The rest resumes as a microtask once the awaited promise settles

3. Sequential vs parallel

javascript
for (const x of xs) await f(x); // sequential
const [a, b] = await Promise.all([f(), g()]); // parallel

start the promises first, then await them together

await in a loop

Each await waits for the previous step, so delays add up. Independent work: start it first, then Promise.all.

JavaScriptPage 4

Error Handling Patterns

1. Errors in a chain

  1. Promise rejects
  2. then skippedevery one
  3. catch runsreturns fulfilled
  4. next then runs
  • A rejection skips every then until a catch
  • catch returns a fulfilled promise, so the chain continues

2. async/await

javascript
try {
  await load();
} catch (e) {
  log(e);
} finally {
  stop();
}

finally runs on success and on failure

  • Unhandled rejection fires unhandledrejection in browsers
  • Crashes Node 15+ by default

3. Where errors escape

CodeResult
then(() => { f(); }) no returnUnhandled
async call without awaitUnhandled
return f() or await f()Caught
throw in executorRejection
throw in later setTimeout callbackNot caught
Remember

Return or await every promise, and end each chain with a catch.

JavaScriptPage 5

Promise.all, allSettled, race and any

1. The four combinators

Fulfils withRejects when
allarray, input orderfirst rejection, at once
allSettledoutcome per inputnever
racefirst to settlefirst to settle rejects
anyfirst fulfilmentall reject: AggregateError
  • all keeps input order, not finish order
  • allSettled item: {status, value} or {status, reason}
  • race plus a timer = the timeout pattern

2. Timeout with race

javascript
const timeout = ms => new Promise((_, rej) =>
  setTimeout(() => rej(new Error('timeout')), ms));
const data = await Promise.race([fetchData(), timeout(3000)]);

3. Losers and interview point

No cancellation

Losing promises are not cancelled; their work keeps running.

  • all vs allSettled? all rejects on the first failure; allSettled never rejects.
  • race vs any? race takes the first to settle; any takes the first success.
  • Does race cancel the losers? No, they keep running.
JavaScriptPage 6

Async Cheat Sheet

1. Run order

  1. Sync codecall stack
  2. Microtasksthen, await, queueMicrotask
  3. Timers, I/Oone task at a time

2. Pick a combinator

NeedUse
Every resultPromise.all
Every outcomePromise.allSettled
First to finishPromise.race
First successPromise.any

3. Loops and timeout

  • Sequential: await inside the loop
  • Parallel: start all, await together
  • Timeout: race the work against a rejecting timer
javascript
for (const x of xs) await f(x);
await Promise.all(xs.map(f));
javascript
await Promise.race([work, new Promise((_, rej) =>
  setTimeout(() => rej(new Error('timeout')), ms))]);
  • forEach with an async callback does not wait
  • Promise left unreturned or unawaited: error escapes the chain
Always

Return or await every promise, and attach a catch.

JavaScriptPage 7

Check yourself

What order does this print? async function f(){ log(1); await null; log(2) } f(); log(3)
  • 1, 3, 2
  • f runs synchronously until the await
  • the rest of f is a microtask, so it runs after the current script
What prints? Promise.reject(1).then(()=>log('a')).catch(()=>log('b')).then(()=>log('c'))
  • b, then c
  • the first then is skipped; the rejection reaches catch
  • catch returns a fulfilled promise, so the chain continues
Spot the bug: items.forEach(async x => await save(x)); log('done')
  • 'done' prints before the saves finish
  • forEach ignores the returned promises, so errors go unhandled
  • fix: await Promise.all(items.map(save))
In Promise.all([p1, p2, p3]), p2 rejects at 10ms and p3 takes 100ms. When does it settle, and what happens to p3?
  • it rejects at 10ms with p2's reason
  • p3 is not cancelled; it keeps running and its result is ignored
  • use allSettled to get every outcome