withRetry
April 12, 2026 ยท View on GitHub
withRetry(options?)
Wraps a fetch function to automatically retry failed requests.
Retries on network errors and configurable HTTP status codes. Only retries idempotent methods by default (GET, HEAD, PUT, DELETE, OPTIONS, TRACE). Uses exponential backoff with jitter by default. Respects the Retry-After response header when present, and ignores malformed values by falling back to backoff.
When all retries are exhausted, the last response is returned (for HTTP status retries) or the last error is thrown (for network errors).
Parameters
options(object)retries(number) - Number of retries after the initial attempt.retries: 2means up to 3 total attempts. Default:2.methods(string[]) - HTTP methods to retry. Non-matching methods pass through without retry. Default:['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'].statusCodes(number[]) - HTTP status codes that trigger a retry. Default:[408, 429, 500, 502, 503, 504].maxRetryAfter(number) - MaximumRetry-Afterduration in milliseconds. If the server requests a longer delay, the response is returned without retrying. Default:60_000.backoff((attemptNumber: number) => number) - Function returning the delay in milliseconds before a retry. Receives the 1-based attempt number that just failed. Default: exponential backoff with jitter (min(1000 * 2^(attempt-1), 30000) * random(0.5, 1.0)).shouldRetry((context) => boolean | Promise<boolean>) - Called after built-in checks pass, before retrying. Returnfalseto stop retrying. The context object has{error?, response?, attemptNumber, retriesLeft}. For network errors,erroris set. For HTTP status retries,responseis set. Do not consume theresponsebody. If you need to inspect the body, clone the response first.
Returns
A function that takes a fetch function and returns a wrapped fetch function with automatic retry.
Note
POST and PATCH are not retried by default because they are not idempotent. Add them to methods if your endpoints are safe to retry.
Tip
In documented pipeline() order, place withRetry before withHttpError so it sees raw responses and can check status codes.
Important
Requests with a one-shot body provided via options.body, such as a ReadableStream or AsyncIterable, are sent as-is and are not retried. Retrying those uploads would require buffering and would change wrapper composition semantics such as upload progress. Requests whose body comes from a bare Request object (no options.body override) are also not retried.
Note
withRetry() resolves replayable request bodies once before the first attempt so later retries can reuse them. When a wrapper couples body resolution to header resolution, withRetry() preserves those resolved headers with the prepared body for every attempt. Other request-scoped header wrappers may still run again on each retry.
If you need retry-time auth behavior, use a wrapper with explicit retry semantics such as withTokenRefresh().
Example
import {withRetry} from 'fetch-extras';
const fetchWithRetry = withRetry({retries: 3})(fetch);
const response = await fetchWithRetry('https://api.example.com/data');
const data = await response.json();
With a custom backoff and conditional retry:
import {withRetry} from 'fetch-extras';
const fetchWithRetry = withRetry({
retries: 5,
backoff: attemptNumber => attemptNumber * 1000, // Linear: 1s, 2s, 3s, ...
shouldRetry({response}) {
// Don't retry if the server says the resource is gone
return response?.status !== 410;
},
})(fetch);
Can be combined with other with* functions:
import {pipeline, withHttpError, withRetry, withBaseUrl, withTimeout} from 'fetch-extras';
const apiFetch = pipeline(
fetch,
withTimeout(10_000),
withBaseUrl('https://api.example.com'),
withRetry({retries: 2}),
withHttpError(),
);
const response = await apiFetch('/users');