typescript / expert
Snippet
Composing Cancellation with AbortSignal.any and timeout
AbortSignal.timeout produces a signal that auto-aborts after the given delay, while AbortSignal.any merges multiple signals so the first to abort wins and the combined signal carries that reason. Wrapping the call in try/catch lets the helper translate any abort — timeout or external cancel — into a single typed error, while genuine task failures rethrow unchanged. The task receives only the combined signal, keeping its body free of cancellation plumbing.
snippet.ts
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class CancelledError extends Error {constructor(public readonly reason: unknown) {super('operation cancelled');this.name = 'CancelledError';}}async function withDeadline<T>(task: (signal: AbortSignal) => Promise<T>,ms: number,external?: AbortSignal,): Promise<T> {const timer = AbortSignal.timeout(ms);const signals = external ? [timer, external] : [timer];const combined = AbortSignal.any(signals);try {return await task(combined);} catch (err) {if (combined.aborted) throw new CancelledError(combined.reason);throw err;}}const userAbort = new AbortController();await withDeadline((s) => doWork(s), 500, userAbort.signal);
Breakdown
1
const timer = AbortSignal.timeout(ms);
Static factory returns a self-aborting signal — no controller or setTimeout needed.
2
const combined = AbortSignal.any(signals);
Aborts as soon as any input signal aborts, propagating the original reason.
3
if (combined.aborted) throw new CancelledError(combined.reason);
Distinguishes a cancellation-induced rejection from an organic task error.
4
return await task(combined);
The task only sees one signal — it does not need to know how cancellation was assembled.
5
await withDeadline((s) => doWork(s), 500, userAbort.signal);
Caller composes its own AbortController with the helper's internal timeout.