Retry Strategies
What This Means
Network requests can fail for transient reasons (temporary outages, network blips). Without retry logic:
- Single failures cause permanent errors
- Users see error states unnecessarily
- Data synchronization fails silently
- Poor reliability on mobile networks
Retry Strategy Types
| Strategy | Best For |
|---|---|
| Immediate retry | Quick transient failures |
| Exponential backoff | Rate limiting, server overload |
| Linear backoff | General transient errors |
| Jitter | Distributed systems, avoiding thundering herd |
General Fixes
Basic Retry with Exponential Backoff
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
// Retry on server errors
if (response.status >= 500) {
throw new Error(`Server error: ${response.status}`);
}
return response;
} catch (error) {
lastError = error;
if (attempt < maxRetries) {
// Exponential backoff: 1s, 2s, 4s...
const delay = Math.pow(2, attempt) * 1000;
console.log(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
await sleep(delay);
}
}
}
throw lastError;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Exponential Backoff with Jitter
async function fetchWithJitteredBackoff(url, options = {}, config = {}) {
const {
maxRetries = 3,
baseDelay = 1000,
maxDelay = 30000,
jitterFactor = 0.5
} = config;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (!response.ok && response.status >= 500) {
throw new Error(`Server error: ${response.status}`);
}
return response;
} catch (error) {
lastError = error;
if (attempt < maxRetries) {
// Calculate delay with exponential backoff
let delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
// Add jitter to prevent thundering herd
const jitter = delay * jitterFactor * Math.random();
delay = delay + jitter;
console.log(`Retry ${attempt + 1}/${maxRetries} after ${Math.round(delay)}ms`);
await sleep(delay);
}
}
}
throw new Error(`Failed after ${maxRetries} retries: ${lastError.message}`);
}
Retry Class with Configuration
class RetryableRequest {
constructor(config = {}) {
this.maxRetries = config.maxRetries ?? 3;
this.baseDelay = config.baseDelay ?? 1000;
this.maxDelay = config.maxDelay ?? 30000;
this.retryableStatuses = config.retryableStatuses ?? [408, 429, 500, 502, 503, 504];
this.retryableErrors = config.retryableErrors ?? ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'];
}
shouldRetry(error, response, attempt) {
if (attempt >= this.maxRetries) return false;
// Check response status
if (response && this.retryableStatuses.includes(response.status)) {
return true;
}
// Check error types
if (error) {
if (error.name === 'AbortError') return false; // Don't retry aborts
if (error.name === 'TypeError') return true; // Network error
if (this.retryableErrors.includes(error.code)) return true;
}
return false;
}
getDelay(attempt, response) {
// Check for Retry-After header
if (response?.headers) {
const retryAfter = response.headers.get('Retry-After');
if (retryAfter) {
const seconds = parseInt(retryAfter, 10);
if (!isNaN(seconds)) {
return seconds * 1000;
}
}
}
// Exponential backoff with jitter
const exponentialDelay = this.baseDelay * Math.pow(2, attempt);
const jitter = exponentialDelay * 0.5 * Math.random();
return Math.min(exponentialDelay + jitter, this.maxDelay);
}
async fetch(url, options = {}) {
let lastError;
let lastResponse;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
const response = await fetch(url, options);
lastResponse = response;
if (response.ok) {
return response;
}
if (this.shouldRetry(null, response, attempt)) {
const delay = this.getDelay(attempt, response);
console.log(`Retry ${attempt + 1}/${this.maxRetries} after ${delay}ms (status: ${response.status})`);
await this.sleep(delay);
continue;
}
return response; // Non-retryable error status
} catch (error) {
lastError = error;
if (this.shouldRetry(error, null, attempt)) {
const delay = this.getDelay(attempt, null);
console.log(`Retry ${attempt + 1}/${this.maxRetries} after ${delay}ms (error: ${error.message})`);
await this.sleep(delay);
continue;
}
throw error;
}
}
throw lastError || new Error(`Request failed with status ${lastResponse?.status}`);
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Usage
const client = new RetryableRequest({
maxRetries: 5,
baseDelay: 500
});
const response = await client.fetch('/api/data');
React Query Retry Configuration
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
// Custom retry logic
retry: (failureCount, error) => {
// Don't retry on 4xx errors
if (error.response?.status >= 400 && error.response?.status < 500) {
return false;
}
return failureCount < 3;
}
},
mutations: {
retry: 1, // Fewer retries for mutations
retryDelay: 1000
}
}
});
SWR Retry Configuration
import useSWR from 'swr';
const { data, error } = useSWR('/api/data', fetcher, {
errorRetryCount: 3,
errorRetryInterval: 5000,
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
// Don't retry on 404
if (error.status === 404) return;
// Don't retry on 401/403
if (error.status === 401 || error.status === 403) return;
// Only retry up to 3 times
if (retryCount >= 3) return;
// Retry after 5 seconds
setTimeout(() => revalidate({ retryCount }), 5000);
}
});
Axios Retry Interceptor
import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.example.com'
});
api.interceptors.response.use(null, async (error) => {
const config = error.config;
// Initialize retry count
config.__retryCount = config.__retryCount || 0;
// Check if we should retry
const shouldRetry = (
config.__retryCount < 3 &&
(!error.response || error.response.status >= 500)
);
if (!shouldRetry) {
return Promise.reject(error);
}
config.__retryCount += 1;
// Calculate delay
const delay = Math.pow(2, config.__retryCount) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
return api(config);
});
Verification
- Test with simulated failures
- Verify exponential backoff timing
- Check max retry limits
- Confirm circuit breaker activates