Reference
yieldless/signal
Deadline helpers for AbortSignal-aware work.
yieldless/signal adds a small missing piece around native cancellation: derived deadline signals that clean up after themselves.
Exports
createTimeoutSignal(timeoutMs, options?): TimeoutSignalwithTimeout(operation, options): Promise<T>withTimeoutSafe(operation, options): Promise<SafeResult<T, E | TimeoutError>>anySignal(signals): LinkedSignal— derive one signal from several parents;undefinedentries are skippedisAbortError(value): boolean— recognizes cancellation reasons (name === "AbortError")class TimeoutError extends Error
Options
| Option | Description |
|---|---|
timeoutMs | Maximum time before the derived signal aborts |
signal | Optional parent signal to inherit cancellation from |
reason | Optional custom timeout reason |
Typical use
import { safeTry } from "yieldless/error";
import { withTimeout } from "yieldless/signal";
const [error, response] = await safeTry(
withTimeout(
(signal) => fetch("https://example.com/api/repos", { signal }),
{
timeoutMs: 5_000,
},
),
);Long-lived scope
Use createTimeoutSignal() when you need to pass one deadline signal across several calls.
import { createTimeoutSignal } from "yieldless/signal";
const deadline = createTimeoutSignal(10_000, {
signal: request.signal,
});
try {
const [error, result] = await runCommandSafe(
"git",
["fetch", "--all"],
{ signal: deadline.signal },
);
} finally {
deadline[Symbol.dispose]();
}Combining cancellation sources
Use anySignal() when an operation must respect several cancellation sources at once, and isAbortError() to keep cancellation out of error logs.
import { anySignal, isAbortError } from "yieldless/signal";
using linked = anySignal([request.signal, shutdown.signal]);
const [error, data] = await loadData(linked.signal);
if (error !== null && !isAbortError(error)) {
logger.error("load failed", error);
}Operational rules
- The parent signal wins if it aborts first.
- Timeout failures use
TimeoutErrorby default. withTimeoutSafe()returns the same failures aswithTimeout()in tuple form.- Disposing the derived signal clears its timer and parent listeners;
anySignalresults should be disposed the same way (or declared withusing). timeoutMsmust be a finite number, zero or greater.
Good fits
- HTTP requests that should not hang forever
- Git commands or subprocesses with a firm deadline
- Any abort-aware operation where you want one shared time budget
Good
Use withTimeout() for one operation.
const [error, response] = await safeTry(
withTimeout(
(signal) => fetch(url, { signal }),
{ timeoutMs: 5_000, signal: request.signal },
),
);Use createTimeoutSignal() for a scope of related operations.
const deadline = createTimeoutSignal(10_000, { signal });
try {
await readProfile(deadline.signal);
await readPermissions(deadline.signal);
} finally {
deadline[Symbol.dispose]();
}Avoid
Do not create ad hoc timers that outlive the request.
const timer = setTimeout(() => controller.abort(), 5_000);
await doWork(controller.signal);Use withTimeout() or dispose the derived signal explicitly so timers and parent listeners are cleaned up.