javascript · memo
In one line: A Promise is a settle-once box for a future value; its reactions always run
later, as microtasks, which the loop drains completely after each task.
async/await is sugar over then: await always suspends.
One thread, no preemption — concurrency, not parallelism; promises are eager and
not cancellable (that’s AbortSignal’s job).
Download PDF Print view LaTeX source
Example — trace it (drawing 3)
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
(async () => { console.log(4); await null; console.log(5); })();
queueMicrotask(() => console.log(6));
new Promise(r => { console.log(7); r(); }) // executor is sync
.then(() => console.log(8));
console.log(9); // 1 4 7 9 3 5 6 8 2
How it works
- The executor runs synchronously inside
new Promise; a throw in it = rejection.thenon an already-settled promise still queues a microtask — never synchronous (no Zalgo). p.then(f)returns a new promise:freturnsv→ fulfilledv; throws → rejected; returns a thenable → adopts it (+2 ticks: a resolve-thenable job, then itsthen).- Errors skip fulfilment handlers down the chain to the next
catch(=then(undefined, f)).finallypasses value/reason through.then(ok, fail):faildoes not catch a throw inok. - Unhandled rejection: rejected with no handler once microtasks drain → browser
unhandledrejectionevent; Node ≥ 15 crashes the process by default. - async fn: runs synchronously up to its first
await, always returns a promise (return vfulfils,throwrejects).await x≈Promise.resolve(x).then(resume, throwInto)— even forawait 1: code after anawaitis always a later microtask. - Inside
try, writereturn await p: plainreturn pleaves the function beforeprejects, so thecatchnever sees it. - Swift: a throwing
TaskGroupchild cancels its siblings;Promise.alldoes not. One JS thread: no data races, but state can change at everyawait(like actor reentrancy).
Combinators
| fulfils with | rejects with | |
|---|---|---|
all | all values, input order | the first rejection (fail-fast) |
allSettled | {status, value|reason}[] | never |
race | first to settle, either way | first to settle, either way |
any | first fulfilment | AggregateError (.errors) if all reject |
[1pt] Empty:
all → [] · any rejects · race
never settles. allSettled ES2020, any ES2021.
Waterfall, forEach, pool
const a = await getA(); const b = await getB(); // tA + tB
const [a, b] = await Promise.all([getA(), getB()]); // max
items.forEach(async x => await save(x)); // NOT awaited!
for (const x of items) await save(x); // sequential
await Promise.all(items.map(x => save(x))); // all at once
async function pool(items, n, work) { // at most n
const out = []; let next = 0;
const lane = async () => { while (next < items.length) {
const i = next++; out[i] = await work(items[i]); } };
await Promise.all(Array.from({ length: n }, lane));
return out; } // next++ is safe: nothing preempts it
Cancel, retry, stream
const ac = new AbortController();
const signal = AbortSignal.any([ac.signal,
AbortSignal.timeout(5000)]);
await fetch(url, { signal }); // ac.abort() -> rejects
async function retry(fn, tries = 5, base = 200) {
for (let i = 0; ; i++) {
try { return await fn(); }
catch (e) { if (i + 1 >= tries) throw e;
const ms = base * 2 ** i * (0.5 + Math.random()); // + jitter
await new Promise(r => setTimeout(r, ms)); } } }
async function* pages(url) { // Symbol.asyncIterator
while (url) { const p = await (await fetch(url)).json();
yield* p.items; url = p.next; } }
for await (const item of pages('/feed')) render(item);
- Abort only signals:
fetchlistens (AbortError/TimeoutError); your code must checksignal.throwIfAborted(). - Retry only idempotent calls on retryable errors (network, 5xx, 429).
breakinfor awaitcalls the generator’sreturn().
Interview traps
pa = getA(); pb = getB(); await pa; await pbis parallel, but apbrejecting duringawait pais unhandled — usePromise.all.Promise.allrejecting does not stop the others; and asetTimeout(0)never beats an already-queued microtask.
Remember
Executor now, reactions later · drain all micro, then one macro · await always yields · abort is a signal, not a kill.
Likely questions
- Micro vs macro? — all micro drain after each task, before render.
- Cancel a promise? — you can’t; cancel the work (
AbortSignal). - Limit concurrency? — N lanes on a shared index (above), or p-limit.