Async operations and retries
Capture promises, retry bounded failures, add backoff and jitter, and propagate cancellation.
For application workflows, prefer Result.gen with Result.await. For a short Promise<Result<...>> pipeline, chain static async combinators with .then(Result.andThenAsync(...)). This page focuses on safely creating those asynchronous Results and controlling retries.
Capture a Promise
const result = await Result.tryPromise({
try: () => fetch(url),
catch: (cause) => new RequestFailed({ url, cause, message: "Request failed" }),
});
The attempt context always includes attempt, starting at 1. Its optional signal is the exact signal supplied in the top-level configuration; when the caller supplies none, signal is undefined. Result.tryPromise does not create an internal abort signal.
Handle fulfilled HTTP errors
fetch rejects for network and cancellation failures, not for HTTP error statuses. Check fulfilled responses explicitly:
class HttpResponseError extends TaggedError("HttpResponseError")<{
status: number;
url: string;
message: string;
}> {}
const successfulResponse = await Result.tryPromise({
try: () => fetch(url),
catch: (cause) => new NetworkError({ url, cause, message: "Network request failed" }),
}).then(
Result.andThen((response: Response) =>
response.ok
? Result.ok(response)
: Result.err(
new HttpResponseError({
status: response.status,
url: response.url,
message: `Request failed with status ${response.status}`,
}),
),
),
);
// Result<Response, NetworkError | HttpResponseError>
A retry configured on Result.tryPromise retries rejected attempts. If selected HTTP statuses are also retryable, convert and retry them in an application operation whose policy explicitly covers status responses.
Static retry policy
const result = await Result.tryPromise(callApi, {
retry: {
times: 3,
delayMs: 100,
backoff: "exponential",
},
});
times counts retries after the initial attempt. Static policies require delayMs and one of:
| Backoff | Delay before retry number n |
|---|---|
constant |
delayMs |
linear |
delayMs × n |
exponential |
delayMs × 2^(n - 1) |
Retry selected errors
retry: {
times: 3,
delayMs: 200,
backoff: "exponential",
shouldRetry: (error, { attempt }) =>
error._tag === "RateLimited" && attempt < 4,
}
shouldRetry is synchronous. If it throws, Result.tryPromise throws Panic.
Error-dependent delays
retry: {
times: 3,
shouldRetry: (error) => error.retryable,
delayMs: (error, { attempt }) => error.retryAfterMs ?? attempt * 250,
}
A dynamic delayMs is the final delay and cannot be combined with backoff or jitter. A throwing callback becomes Panic.
Jitter
retry: {
times: 3,
delayMs: 100,
backoff: "exponential",
jitter: 0.3,
}
A numeric jitter from 0 through 1 shortens a static delay by up to that fraction. true means full jitter. Invalid, non-finite, or out-of-range numbers throw Panic before the first attempt.
Cancellation
const controller = new AbortController();
const result = await Result.tryPromise(({ signal }) => fetch(url, { signal }), {
signal: controller.signal,
retry: { times: 3, delayMs: 100, backoff: "constant" },
});
Aborting stops a pending retry delay and prevents later attempts. The active operation is cancelled only if your callback forwards signal to an abort-aware API. The latest typed Err is returned when retry scheduling stops.