// ───────────────────────────────────────────────────────────────────────────── // JS ASYNC — PROMISES, ASYNC/AWAIT, EVENT LOOP ORDER // ───────────────────────────────────────────────────────────────────────────── // COMBINATORS : Promise.all (fail-fast), allSettled (never rejects), race (first // settled), any (first fulfilled; AggregateError if all reject) // ORDER : sync code -> microtasks (promise callbacks, queueMicrotask, // process.nextTick first in Node) -> macrotasks (setTimeout, I/O) // TRAPS : forEach(async) doesn't await; await in a loop = sequential; // forgetting return in .then; unhandled rejection; async fn always // returns a Promise; try/catch only catches awaited rejections // SAY : "microtask queue", "fail-fast", "sequential vs concurrent", // "concurrency limit / pool", "backoff with jitter", "idempotent // retry", "AbortController for cancellation/timeouts" // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); const tick = (v, ms = 0) => new Promise(res => setTimeout(() => res(v), ms)); const fail = (msg, ms = 0) => new Promise((_, rej) => setTimeout(() => rej(new Error(msg)), ms)); (async () => { // ── 1. Event loop order — the classic "what prints?" const order = []; setTimeout(() => order.push('timeout'), 0); Promise.resolve().then(() => order.push('micro')); process.nextTick(() => order.push('nextTick')); order.push('sync'); await tick(null, 5); assert.deepEqual(order, ['sync', 'nextTick', 'micro', 'timeout']); // ── 2. Combinators assert.deepEqual(await Promise.all([tick(1), tick(2), 3]), [1, 2, 3]); // non-promises pass through await assert.rejects(Promise.all([tick(1), fail('boom')]), /boom/); // fail-fast const settled = await Promise.allSettled([tick(1), fail('x')]); assert.deepEqual(settled.map(s => s.status), ['fulfilled', 'rejected']); assert.equal(settled[1].reason.message, 'x'); assert.equal(await Promise.race([tick('slow', 20), tick('fast', 1)]), 'fast'); assert.equal(await Promise.any([fail('a'), tick('ok', 1)]), 'ok'); await assert.rejects(Promise.any([fail('a'), fail('b')]), AggregateError); // ── 3. Sequential vs concurrent const t0 = Date.now(); for (const ms of [10, 10, 10]) await tick(null, ms); // ~30ms, one after another const seq = Date.now() - t0; const t1 = Date.now(); await Promise.all([10, 10, 10].map(ms => tick(null, ms))); // ~10ms, all at once const par = Date.now() - t1; assert.equal(seq > par, true); // forEach(async ...) does NOT wait: arr.forEach(async x => await f(x)) -> fire-and-forget. Use for..of or Promise.all(map). // ── 4. Concurrency limit (pool of N workers) — asked constantly async function mapLimit(items, limit, fn) { const results = new Array(items.length); let next = 0; async function worker() { while (next < items.length) { const i = next++; results[i] = await fn(items[i], i); } } await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker)); return results; } let inFlight = 0, peak = 0; const out = await mapLimit([1, 2, 3, 4, 5], 2, async x => { peak = Math.max(peak, ++inFlight); await tick(null, 2); inFlight--; return x * 2; }); assert.deepEqual(out, [2, 4, 6, 8, 10]); assert.equal(peak, 2); // ── 5. Error handling shapes async function mayThrow(bad) { if (bad) throw new Error('nope'); return 'fine'; } assert.equal(await mayThrow(false), 'fine'); try { await mayThrow(true); assert.fail('unreachable'); } catch (e) { assert.equal(e.message, 'nope'); } const safe = await mayThrow(true).catch(e => `caught ${e.message}`); assert.equal(safe, 'caught nope'); // Without await, try/catch does NOT catch: try { mayThrow(true) } catch {} -> unhandled rejection. const r = await Promise.reject(new Error('z')).then(() => 'no', () => 'yes'); // second arg = onRejected assert.equal(r, 'yes'); let finallyRan = false; await tick(1).finally(() => { finallyRan = true; }); assert.equal(finallyRan, true); // ── 6. Retry with exponential backoff + jitter async function retry(fn, { attempts = 3, baseMs = 1, factor = 2 } = {}) { let lastErr; for (let i = 0; i < attempts; i++) { try { return await fn(i); } catch (e) { lastErr = e; const delay = baseMs * factor ** i * (0.5 + Math.random() / 2); // jitter await tick(null, delay); } } throw lastErr; } let tries = 0; assert.equal(await retry(async () => { if (++tries < 3) throw new Error('flaky'); return 'ok'; }), 'ok'); assert.equal(tries, 3); await assert.rejects(retry(async () => { throw new Error('always'); }, { attempts: 2 }), /always/); // ── 7. Timeout / cancellation function withTimeout(promise, ms) { let timer; const timeout = new Promise((_, rej) => { timer = setTimeout(() => rej(new Error('timeout')), ms); }); return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)); } assert.equal(await withTimeout(tick('v', 1), 50), 'v'); await assert.rejects(withTimeout(tick('v', 50), 1), /timeout/); const ac = new AbortController(); const abortable = new Promise((res, rej) => { const t = setTimeout(() => res('done'), 50); ac.signal.addEventListener('abort', () => { clearTimeout(t); rej(ac.signal.reason); }, { once: true }); }); ac.abort(new Error('cancelled')); await assert.rejects(abortable, /cancelled/); // fetch(url, { signal: ac.signal }) and AbortSignal.timeout(ms) use the same mechanism. // ── 8. Promisify a callback API / build a promise by hand / deferred const cbStyle = (x, cb) => setTimeout(() => (x < 0 ? cb(new Error('neg')) : cb(null, x * 2)), 0); const promisified = x => new Promise((res, rej) => cbStyle(x, (err, v) => (err ? rej(err) : res(v)))); assert.equal(await promisified(2), 4); await assert.rejects(promisified(-1), /neg/); const { promise, resolve } = Promise.withResolvers?.() ?? (() => { let resolve; const promise = new Promise(r => (resolve = r)); return { promise, resolve }; })(); resolve(7); assert.equal(await promise, 7); // Node 22+ has withResolvers // ── 9. Chaining gotchas const chained = await Promise.resolve(1).then(v => v + 1).then(v => { v + 1; }).then(v => v); assert.equal(chained, undefined); // forgot to return -> undefined const flattened = await Promise.resolve(1).then(v => Promise.resolve(v + 1)); // returned promise is unwrapped assert.equal(flattened, 2); async function returnsPromise() { return 1; } assert.equal(returnsPromise() instanceof Promise, true); // ── 10. Async iteration / generators async function* pages() { for (let p = 1; p <= 3; p++) { await tick(null, 1); yield p; } } const got = []; for await (const p of pages()) got.push(p); assert.deepEqual(got, [1, 2, 3]); console.log('ok: javascript/async-promises.js'); })().catch(e => { console.error(e); process.exitCode = 1; });