Overview

Timeout

Wrap a behavior with timeout and child AbortSignal propagation.

timeoutcancellation

Install

$
Terminal window
pnpm dlx shadcn@latest add https://archyn.net/r/timeout.json

Copy the files below into the target paths shown in each tab.

ts@lib/flow/timeout.ts
import { err, type Result } from "./core/result";
import {
abortController,
createLinkedController,
type Behavior,
type Cancelled,
type RuntimeContext,
type Thrown,
contextWithSignal,
resultFromSignal,
runBehavior,
thrown,
} from "./core/runtime";
/**
* Domain error returned when a timeout expires before source completion.
*/
export type TimeoutError = {
kind: "timeout";
};
/**
* Timing-related errors added by timeout and delay helpers.
*/
export type TimingError = TimeoutError | Cancelled;
/**
* Wraps a behavior with a timeout.
*
* The source runs with a child signal linked to the parent. If the timer expires first, the child
* is aborted and a timeout domain error is returned.
*
* @typeParam I - Source input type.
* @typeParam O - Source output type.
* @typeParam E - Source domain error type.
* @typeParam C - Custom context type.
* @param source - Behavior to run with a time limit.
* @param ms - Timeout in milliseconds.
* @returns A behavior with the source result or timing errors.
*
* @example
* ```ts
* const boundedLoad = timeout(loadUser, 1000);
* ```
*/
export function timeout<I, O, E, C extends object>(
source: Behavior<I, O, E, C>,
ms: number,
): Behavior<I, O, E | TimingError, C> {
return async (input, context) => runWithTimeout(source, input, context, ms);
}
/**
* Runs one behavior execution with a timeout.
*
* This advanced helper powers `timeout` and is exported for custom helper authors that need direct
* control over input and context.
*
* @typeParam I - Source input type.
* @typeParam O - Source output type.
* @typeParam E - Source domain error type.
* @typeParam C - Custom context type.
* @param source - Behavior to execute.
* @param input - Input passed to the behavior.
* @param context - Runtime context for this execution.
* @param timeoutMs - Timeout in milliseconds.
* @returns A result containing source output, source domain error, timing error, or thrown error.
*/
export function runWithTimeout<I, O, E, C extends object>(
source: Behavior<I, O, E, C>,
input: I,
context: RuntimeContext<C>,
timeoutMs: number,
): Promise<Result<O, E | TimingError | Thrown>> {
if (context.signal.aborted) {
return Promise.resolve(resultFromSignal(context.signal));
}
const { controller, unlink } = createLinkedController(context.signal);
const childContext = contextWithSignal(context, controller.signal);
return new Promise((resolve) => {
let settled = false;
const settle = (result: Result<O, E | TimingError | Thrown>) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
context.signal.removeEventListener("abort", onAbort);
unlink();
resolve(result);
};
const onAbort = () => {
abortController(controller, context.signal.reason);
settle(resultFromSignal(context.signal));
};
const timer = setTimeout(
() => {
settle(err({ kind: "timeout" }));
abortController(controller, { kind: "timeout" });
},
Math.max(0, timeoutMs),
);
context.signal.addEventListener("abort", onAbort, { once: true });
runBehavior(source, input, childContext)
.then(settle)
.catch((error: unknown) => settle(thrown(error)));
});
}

Update the import paths to match your project setup.

Bound a Behavior with a timeout and cancel its child work when the deadline expires.

Usage

timeout wraps an existing Behavior with a child AbortSignal. If the deadline wins, the wrapper returns { kind: "timeout" } and aborts the child controller so cooperative work can stop.

Use it when:

  • Protect network calls, background work, or user-triggered actions from running too long.
  • Preserve the source Behavior output and domain error types.
  • Convert deadlines into typed failures instead of throwing.
import { task } from "@/lib/flow/task";
import { timeout } from "@/lib/flow/timeout";
const fetchReport = task(() => fetch("/api/report").then((response) => response.json()));
const fetchReportWithinOneSecond = timeout(fetchReport, 1000);

Behavior

The wrapper links the parent signal to a child controller, starts the source Behavior, and races it against a timer. Settling clears the timer and unlinks abort listeners.

Source domain errors pass through unchanged. Parent cancellation returns Cancelled. Deadline expiry returns TimeoutError and aborts child work with the timeout reason.

Types

The source domain error type is unioned with TimingError.

Import from @/lib/flow/timeout.

Composition Notes

Place timeout around the scope whose total elapsed time should be bounded.

Do not use timeout as a substitute for cancelling underlying platform work; the source should still observe its signal.

Source

ts@lib/flow/timeout.ts
import { err, type Result } from "./core/result";
import {
abortController,
createLinkedController,
type Behavior,
type Cancelled,
type RuntimeContext,
type Thrown,
contextWithSignal,
resultFromSignal,
runBehavior,
thrown,
} from "./core/runtime";
/**
* Domain error returned when a timeout expires before source completion.
*/
export type TimeoutError = {
kind: "timeout";
};
/**
* Timing-related errors added by timeout and delay helpers.
*/
export type TimingError = TimeoutError | Cancelled;
/**
* Wraps a behavior with a timeout.
*
* The source runs with a child signal linked to the parent. If the timer expires first, the child
* is aborted and a timeout domain error is returned.
*
* @typeParam I - Source input type.
* @typeParam O - Source output type.
* @typeParam E - Source domain error type.
* @typeParam C - Custom context type.
* @param source - Behavior to run with a time limit.
* @param ms - Timeout in milliseconds.
* @returns A behavior with the source result or timing errors.
*
* @example
* ```ts
* const boundedLoad = timeout(loadUser, 1000);
* ```
*/
export function timeout<I, O, E, C extends object>(
source: Behavior<I, O, E, C>,
ms: number,
): Behavior<I, O, E | TimingError, C> {
return async (input, context) => runWithTimeout(source, input, context, ms);
}
/**
* Runs one behavior execution with a timeout.
*
* This advanced helper powers `timeout` and is exported for custom helper authors that need direct
* control over input and context.
*
* @typeParam I - Source input type.
* @typeParam O - Source output type.
* @typeParam E - Source domain error type.
* @typeParam C - Custom context type.
* @param source - Behavior to execute.
* @param input - Input passed to the behavior.
* @param context - Runtime context for this execution.
* @param timeoutMs - Timeout in milliseconds.
* @returns A result containing source output, source domain error, timing error, or thrown error.
*/
export function runWithTimeout<I, O, E, C extends object>(
source: Behavior<I, O, E, C>,
input: I,
context: RuntimeContext<C>,
timeoutMs: number,
): Promise<Result<O, E | TimingError | Thrown>> {
if (context.signal.aborted) {
return Promise.resolve(resultFromSignal(context.signal));
}
const { controller, unlink } = createLinkedController(context.signal);
const childContext = contextWithSignal(context, controller.signal);
return new Promise((resolve) => {
let settled = false;
const settle = (result: Result<O, E | TimingError | Thrown>) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
context.signal.removeEventListener("abort", onAbort);
unlink();
resolve(result);
};
const onAbort = () => {
abortController(controller, context.signal.reason);
settle(resultFromSignal(context.signal));
};
const timer = setTimeout(
() => {
settle(err({ kind: "timeout" }));
abortController(controller, { kind: "timeout" });
},
Math.max(0, timeoutMs),
);
context.signal.addEventListener("abort", onAbort, { once: true });
runBehavior(source, input, childContext)
.then(settle)
.catch((error: unknown) => settle(thrown(error)));
});
}