retry
Composable retry policies with a built-in runner, structured terminal errors, and AbortSignal cancellation.
retry gives you reusable retry policies and a built-in runner so you can add resilient retries to async operations without rewriting orchestration loops.
Motivation
A hand-written retry loop is usually a for loop with setTimeout, and it is easy to get wrong in ways that only show up under load. Without jitter, every client that lost connection to a service retries at the exact same moment once it comes back, causing a new spike right when the service is trying to recover.
Without cancellation, a retry loop can keep running — and keep hitting the network — after the result is no longer needed, for example after a user navigates away.
retry gives you this behavior as a built-in option, instead of something you write from scratch. exponentialBackoff and linearBackoff support jitter ("full" or "equal", the same strategies AWS recommends) through the jitter option — turn it on and delays are randomized instead of fixed.
Every retry loop also accepts an AbortSignal, checked before each attempt and while waiting between attempts, so an abort stops the next attempt or delay from starting; it does not interrupt an attempt that is already running. You get retry policies as values you configure once and reuse, instead of logic you have to get right from scratch in every project.
Features
- Built-in policies:
fixedDelay(...),linearBackoff(...), andexponentialBackoff(...). - Jitter:
"full"or"equal"jitter onexponentialBackoff/linearBackoff, to avoid synchronized retries against a shared upstream. - A shared runner via
runRetryPolicy(policy, execute, options?)with attempt-aware callbacks and custom sleep injection. - Structured terminal errors:
RetryErroron exhaustion,AbortErroron cancellation. - Non-throw mode (
throwOnExhausted: false) returns aRetryRunResultinstead of throwing. - Cancellation through
AbortSignal, checked before, between, and during retries. - Custom policies as plain objects implementing
RetryPolicy— just anext(...)function, no subclassing. - Optional logging via a
logger?: Loggeroption fromlogger— omit it and there’s zero logging overhead. - Native OpenTelemetry — retry decisions as events on the caller’s active span, plus a
retry.attemptscounter, no-op until an SDK is registered. - Tree-shakeable — policies are functions returning plain objects, not classes; unused policies are dropped by any modern bundler.
Quick Start
import { ConsoleLogger } from "@zap-studio/logger";
import { exponentialBackoff, runRetryPolicy } from "@zap-studio/retry";
import { $fetch } from "@zap-studio/fetch";
const logger = new ConsoleLogger({ minLevel: "debug" });
const policy = exponentialBackoff({
maxAttempts: 5,
baseDelayMs: 100,
maxDelayMs: 2_000,
});
const data = await runRetryPolicy(
policy,
async () => {
const response = await $fetch("https://api.example.com/users", {
throwOnFetchError: true,
});
return await response.json();
},
{ logger },
);
Runtime Support
| Runtime | Minimum version |
|---|---|
| Node.js | 18.0.0 |
| Bun | 1.0.0 |
| Deno | 1.42 |
| Cloudflare Workers | Any current release |
| Browsers | Chrome/Edge 98, Firefox 97, Safari 15.4 |
Cancellation relies on AbortSignal.reason, which sets the browser minimums above. Deno 1.42 is the first release that can install packages from JSR (deno add jsr:@zap-studio/retry).
Learn More
- Getting Started — install and build your first retry policy step by step
- Built-in Policies: fixedDelay
- Built-in Policies: linearBackoff
- Built-in Policies: exponentialBackoff
- Jitter
- Shared Runner
- Non-throw Mode
- Structured Errors
- Cancellation
- Custom Policies
- Logging
- OpenTelemetry