# TypeScript Async — Derived Application > Applies Atelier's domain principles to TypeScript async specifically. > Derives from `domains/` docs; introduces no new P-rules (D-063). > See `languages/typescript.md` for the language first-principles stub. ## Promises and AbortSignal (Concurrency P7 Cancellation Support, C1 Correctness) - **Every async function accepts an optional `AbortSignal`:** cancellation is a first-class parameter, not a side channel. The signal propagates to `fetch`, `setTimeout`, and downstream awaits. - **`AbortController` is the producer side; `AbortSignal` is the consumer side:** a function takes a `signal` (read-only), the caller owns the `controller` and decides when to abort. - **Abort propagates as a rejected `Promise`:** `fetch` rejects with `AbortError`; downstream code sees the rejection, not a silent no-op. This preserves `errors/P5` (recoverable when possible) — the caller can distinguish cancellation from a real failure. - **Applies `concurrency/P7`:** no async operation runs without a path to cancel it. A long-running `await` with no signal is a hung request. - **Never swallow `AbortError`:** re-throw or handle distinctly; cancellation is the caller's intent, not an error to log. ```typescript async function fetchUser(id: UserId, signal?: AbortSignal): Promise { const ctrl = new AbortController(); signal?.addEventListener('abort', () => ctrl.abort()); const res = await fetch(`/users/${id}`, { signal: ctrl.signal }); if (!res.ok) throw new HttpError(res.status); return res.json() as Promise; } // caller controls cancellation const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 5000); try { const u = await fetchUser(id, ctrl.signal); } finally { clearTimeout(timer); } ``` ## async/await Discipline (Concurrency P8 Timeout Discipline, C2 Clarity) - **`await` is the only async primitive in application code:** no `.then` chains, no callback pyramids. `async`/`await` reads top-to-bottom (Clarity C2). - **Never `await` in a hot loop without batching:** sequential `await` in a `for` loop is O(n) latency. Use `Promise.all` for parallelism; `for await...of` only for genuine streams. - **`Promise.race` for a timeout:** every external `await` has a deadline. `Promise.race([op, timeout])` rejects when the deadline passes. - **`return` vs `return await`:** inside `try`/`finally`, `return await` runs the `finally`; bare `return` of a Promise defers the `finally` to the microtask. Prefer `return await` when cleanup must run. - **Applies `concurrency/P8`:** a bare `await` with no timeout is an unbounded wait. External calls (network, disk) always race against a deadline. ```typescript async function fetchWithTimeout(url: string, ms = 5000, signal?: AbortSignal): Promise { const ctrl = new AbortController(); signal?.addEventListener('abort', () => ctrl.abort()); const timer = new Promise((_, reject) => setTimeout(() => reject(new TimeoutError(ms)), ms) ); try { return await Promise.race([fetch(url, { signal: ctrl.signal }), timer]); } finally { clearTimeout(timer); // cleanup runs on success and on race-loss } } ``` ## Error Handling in Async (Errors P5 Recoverable When Possible, Errors P1 Errors are Data) - **Catch `unknown`, narrow with a type guard:** `catch (e: unknown)` — TS does not infer the error type. `instanceof` or a discriminator narrows it. - **Retry with backoff for transient failures:** network blips are recoverable (Errors P5). Exponential backoff with jitter, capped retry count, and an `AbortSignal`-aware `setTimeout`. - **No retry for non-idempotent operations:** a `POST` that creates a resource is not safely retryable without an idempotency key (applies `api/P6` Idempotency). - **Typed errors over `Error` subclasses:** a discriminated union `AppError = Network | Timeout | Cancelled` carries context (Errors P4 Preserve Context) without `instanceof` chains. ```typescript async function fetchRetry(url: string, attempts = 3, signal?: AbortSignal): Promise { for (let i = 0; i < attempts; i++) { try { return await fetchWithTimeout(url, 5000, signal); } catch (e: unknown) { if (e instanceof AbortError) throw e; // do not retry cancellation if (e instanceof TimeoutError && i < attempts - 1) { await sleep(jitter(i), signal); // backoff before retry continue; } throw e; } } throw new Error('unreachable'); } ``` ## Cancellation Propagation (Concurrency P7 Cancellation Support, Concurrency P9 Bounded Queues) - **One signal, many consumers:** pass the same `AbortSignal` to every async call in a request. Aborting once cancels the whole tree. - **Bounded concurrency with a semaphore:** a `Semaphore(N)` wrapping `Promise.all` caps in-flight requests (Concurrency P9 — bounded queues). Unbounded `Promise.all` on a 10k-item array exhausts file descriptors. - **Cancellation is cooperative, not preemptive:** a long synchronous block inside an `async` function ignores the signal. Yield with `await Promise.resolve()` periodically in CPU-bound loops, or move to a worker. - **Applies `messaging/delivery-semantics`:** a cancelable async operation is an at-most-once delivery — the caller may stop listening, the result may or may not arrive. Retry-on-cancel is at-least-once; the caller must declare which. ```typescript async function mapBounded(items: readonly T[], fn: (t: T, s: AbortSignal) => Promise, limit = 8, signal?: AbortSignal): Promise { const ctrl = new AbortController(); signal?.addEventListener('abort', () => ctrl.abort()); const results: U[] = new Array(items.length); let next = 0; const workers = Array.from({ length: limit }, async () => { while (true) { const i = next++; if (i >= items.length) break; if (ctrl.signal.aborted) throw new AbortError(); results[i] = await fn(items[i], ctrl.signal); } }); await Promise.all(workers); return results; } ``` ## Cross-References - `domains/concurrency/patterns.md` — the cancellation/timeout/semaphore patterns applied here. - `domains/concurrency/first-principles.md` — Concurrency P7 Cancellation Support, P8 Timeout Discipline, P9 Bounded Queues. - `domains/messaging/delivery-semantics.md` — at-most-once vs at-least-once framing for async retry/cancel (IDEATE-40). - `domains/errors/patterns.md` — typed async errors and retry-with-backoff. - `languages/ts-types.md` — `Result` and discriminated `AppError` used in async error handling. - `languages/ts-tooling.md` — `no-floating-promises` lint rule that enforces these awaits.