Event Loop and Promises
Lookup sheet for stage 1. The question it exists to answer: in what order does this run, and what happens to the errors?
Ordering
One thread. No callback interrupts running code. After the current synchronous run finishes:
- Drain the entire microtask queue, including microtasks queued by microtasks.
- Take one task.
- Repeat.
finishes, nothing interrupts it"] Q{"microtask
queue empty?"} M["run one microtask
which may queue more"] T["run exactly one task"] S --> Q Q -- no --> M M --> Q Q -- yes --> T T --> Q
The two loops are not symmetric, and that is the whole shape: the inner one runs until the queue is empty, the outer one takes a single task each time round.
| Source | Queue |
|---|---|
Promise.then, catch, finally | microtask |
the continuation after an await | microtask |
queueMicrotask | microtask |
process.nextTick (Node only) | ahead of other microtasks |
setTimeout, setInterval | task |
| I/O callbacks | task |
setImmediate (Node only) | task, after I/O in the same turn |
Worked example:
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// 1 4 3 2
Two consequences worth stating:
- A synchronous loop that takes one second delays every timer and every response by one second. There is no pre-emption.
- An unbroken chain of promise reactions starves tasks completely, because the microtask queue must be empty before a task runs.
await
- An
asyncfunction runs synchronously until its firstawait, then returns a promise. awaitalways yields, even on an already-resolved value or a non-promise. Everything after it is a microtask.- Local bindings survive the suspension, because the continuation is a closure.
awaitin afor...ofloop is sequential.awaitinsideforEach,maporfilterawaits nothing.
Combinators
| Combinator | Settles when | On rejection | Result shape |
|---|---|---|---|
Promise.all | all fulfil | rejects on first | array of values |
Promise.allSettled | all settle | never rejects | array of {status, value \| reason} |
Promise.race | first settles | adopts it | one outcome |
Promise.any | first fulfils | rejects only if all do | one value, or AggregateError |
Promise.all rejecting does not cancel the others. They continue, and a second rejection with no handler becomes an unhandled rejection. Use allSettled when every outcome matters.
Sequential against concurrent
const a = await getA(); // 100ms
const b = await getB(); // + 100ms = 200ms total
const [a2, b2] = await Promise.all([getA(), getB()]); // 100ms total
Nothing in the syntax marks the difference. Check every pair of consecutive awaits for a dependency that may not exist.
Mistakes the compiler does not report
| Mistake | Symptom | Fix |
|---|---|---|
doWork() without await | errors vanish, ordering undefined, process may exit | await, or .catch(...) deliberately |
arr.forEach(async ...) | nothing waits, rejections float | for...of with await, or Promise.all(arr.map(...)) |
await in a loop when calls are independent | latency is the sum | Promise.all |
Promise.all where a partial failure is normal | one rejection hides the rest | Promise.allSettled |
try around a synchronous call taking an async callback | the rejection escapes the try | move the await inside the try |
| blocking loop between awaits | timers and responses stall | break the work up, or move it off-thread |
The lint rule no-floating-promises catches the first two. The compiler alone does not.
Cancellation
There is no way to cancel a promise. The mechanism is AbortController: pass its signal to the operation, and call abort() to make the operation itself reject. A promise with nothing listening still runs to completion.