Handbooks / JavaScript / Chapter 4
Event Loop and Async
7 pages · ~4 min✓ Reviewed
Builds on this and Prototypes. Next up: Modern JavaScript Features.
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
| Queue | Holds |
|---|---|
| Task (macrotask) | setTimeout, setInterval, I/O, UI events |
| Microtask | promise reactions, queueMicrotask, MutationObserver |
3. The Loop Rule
- Run one task
- Drain all microtasks
- Render if needed
- 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.
Execution Order and setTimeout
1. Order of a Script
- Sync codewhole stack
- Microtasksdrain all
- 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.
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 does | New promise |
|---|---|
| Returns a value | fulfils with it |
| Returns a promise | adopts its outcome |
| Throws | rejects with the error |
2. Microtasks and await
then/catch/finallycallbacks always run as microtasks- Even when the promise is already settled
- An
asyncfunction always returns a promise awaitpauses 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.
Error Handling Patterns
1. Errors in a chain
- Promise rejects
- then skippedevery one
- catch runsreturns fulfilled
- next then runs
- A rejection skips every
thenuntil acatch catchreturns 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
| Code | Result |
|---|---|
then(() => { f(); }) no return | Unhandled |
async call without await | Unhandled |
return f() or await f() | Caught |
throw in executor | Rejection |
throw in later setTimeout callback | Not caught |
Remember
Return or await every promise, and end each chain with a catch.
Promise.all, allSettled, race and any
1. The four combinators
| Fulfils with | Rejects when | |
|---|---|---|
all | array, input order | first rejection, at once |
allSettled | outcome per input | never |
race | first to settle | first to settle rejects |
any | first fulfilment | all reject: AggregateError |
allkeeps input order, not finish orderallSettleditem:{status, value}or{status, reason}raceplus 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.
Async Cheat Sheet
1. Run order
- Sync codecall stack
- Microtasksthen, await, queueMicrotask
- Timers, I/Oone task at a time
2. Pick a combinator
| Need | Use |
|---|---|
| Every result | Promise.all |
| Every outcome | Promise.allSettled |
| First to finish | Promise.race |
| First success | Promise.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))]);
forEachwith 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.
Check yourself
What order does this print? async function f(){ log(1); await null; log(2) } f(); log(3)
- 1, 3, 2
fruns synchronously until theawait- the rest of
fis 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
thenis skipped; the rejection reachescatch catchreturns 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
forEachignores 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
allSettledto get every outcome