-
Notifications
You must be signed in to change notification settings - Fork 0
Cookbook: retries
The retry service re-issues failed requests with exponential backoff — but only when it is safe to do so. It is attached by default and off until a request asks for retries. A retryable failure is a network error, a 5xx, or a 429.
Set the retry option: true uses the configured default (io.retry.retries, 2), a number caps the extra attempts, and an object unlocks the full shape {retries, initDelay, force, nextDelay, continueRetries}.
import io from 'double-meh';
// initial call + up to 3 retries
const data = await io.get('https://api.example.com/flaky', null, {retry: 3});
// same thing, object form
await io.get('https://api.example.com/flaky', null, {retry: {retries: 3}});
// defaults: up to io.retry.retries (2) retries
await io.get('https://api.example.com/flaky', null, {retry: true});Retries only fire for requests that are safe to repeat. GET, HEAD, OPTIONS, PUT, and DELETE are always retried. A bare POST is not — a network blip could mean the first attempt already succeeded, so blindly resending risks a duplicate.
// not retried: repeating a POST could create two resources
await io.post('https://api.example.com/orders', order, {retry: 3});Give the server a way to deduplicate. idempotencyKey: true generates a UUID once and reuses it across every retry; a string uses your own key. (ifNoneMatch: '*' also unlocks retries for create-if-absent POSTs.)
await io.post('https://api.example.com/orders', order, {
retry: 3,
idempotencyKey: true // one Idempotency-Key header, reused on each attempt
});A PATCH becomes retryable with ifMatch or as: 'merge-patch'. To override the safety gate entirely, force it with retry: {force: true}; disable per request with retry: false.
When a 429 or 503 carries a Retry-After header, the service waits exactly that long — seconds or an HTTP date — instead of using its own backoff. Every pause, Retry-After included, is clamped to io.retry.maxDelay (default 30 s), so a hostile or misconfigured server cannot stall you for an hour.
await io.get('https://api.example.com/rate-limited', null, {retry: 5});
// a 429 with `Retry-After: 2` pauses 2s before the next attemptcontinueRetries(response, attempt, options) decides from each response whether to go again. With retries: 0 it is a pure polling loop — the predicate alone drives it; with a positive retries it is bounded by that count. (The predicate sees responses, not network errors — those still follow the retries count.)
// re-issue the GET while the server answers 202 Accepted
const report = await io.get('https://api.example.com/reports/42', null, {
retry: {retries: 0, initDelay: 1000, continueRetries: response => response.status === 202}
});If a DELETE is retried and a later attempt returns 404, the resource is already gone — the service reports 204 No Content rather than surfacing the 404.
const env = await io.full.delete('https://api.example.com/things/1', null, {retry: 3});
env.status; // 204 even if the retry saw a 404initDelay sets the first pause (default 100 ms; use 0 in tests). nextDelay(delay, attempt, options) computes each subsequent pause — the default doubles it, capped at 5 s. Both can be set per request inside the retry object or globally on io.retry (which also holds retries and maxDelay).
await io.get(url, null, {retry: {retries: 4, initDelay: 250}});
io.retry.initDelay = 200;
io.retry.nextDelay = (delay, attempt) => Math.min(delay * 2, 10000) + Math.random() * 100;Note: an aborted request is never retried — an abort (yours or a timeout) always surfaces immediately. A request whose body is a ReadableStream is never retried either — the stream cannot be replayed.
Guides
Concepts
Services
Modules
- fetch transport
- keys & URLs
- helpers
- records
- sse
- storage backends
- compression encoders
- Service Worker integration
Cookbook