Yieldless
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?): TimeoutSignal
  • withTimeout(operation, options): Promise<T>
  • class TimeoutError extends Error

Options

OptionDescription
timeoutMsMaximum time before the derived signal aborts
signalOptional parent signal to inherit cancellation from
reasonOptional 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]();
}

Operational rules

  • The parent signal wins if it aborts first.
  • Timeout failures use TimeoutError by default.
  • Disposing the derived signal clears its timer and parent listener.
  • timeoutMs must be 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.

On this page