Implementing Retry Logic with Exponential Backoff
Retry logic with exponential backoff improves resilience for transient network failures.
Basic Retry Pattern
async function retryWithBackoff<T>(
operation: () => Promise<T>,
maxRetries: number = 3
): Promise<T> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
// Don't retry on last attempt
if (attempt === maxRetries) {
throw error;
}
// Check if error is retryable
if (!isRetryableError(error)) {
throw error;
}
// Exponential backoff
const backoffMs = Math.pow(2, attempt) * 1000;
console.log(`Attempt ${attempt} failed, retrying in ${backoffMs}ms...`);
await sleep(backoffMs);
}
}
throw lastError;
}
Sleep Utility
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
Retryable Error Detection
function isRetryableError(error: any): boolean {
// HTTP status codes that are retryable
const retryableStatusCodes = [408, 429, 500, 502, 503, 504];
if (error.code && retryableStatusCodes.includes(error.code)) {
return true;
}
// Network errors
const errorMessage = error.message?.toLowerCase() || '';
const retryableMessages = [
'timeout',
'econnreset',
'enotfound',
'econnrefused',
'socket hang up',
'network',
];
return retryableMessages.some(msg => errorMessage.includes(msg));
}
Backoff Strategies
Exponential Backoff
const backoffMs = Math.pow(2, attempt) * 1000;
// 1s, 2s, 4s, 8s, 16s...
Exponential Backoff with Jitter
function calculateBackoff(attempt: number, baseMs: number = 1000): number {
const exponential = Math.pow(2, attempt) * baseMs;
const jitter = Math.random() * baseMs; // Add randomness
return exponential + jitter;
}
Linear Backoff
const backoffMs = attempt * 2000; // 2s, 4s, 6s...
Fixed Delay
const backoffMs = 3000; // Always 3 seconds
With Max Delay Cap
function calculateBackoffWithCap(
attempt: number,
baseMs: number = 1000,
maxMs: number = 30000
): number {
const exponential = Math.pow(2, attempt) * baseMs;
return Math.min(exponential, maxMs);
}
Complete Retry Class
class RetryManager {
constructor(
private maxRetries: number = 3,
private baseDelayMs: number = 1000,
private maxDelayMs: number = 30000
) {}
async execute<T>(
operation: () => Promise<T>,
isRetryable: (error: any) => boolean = isRetryableError
): Promise<T> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (attempt === this.maxRetries || !isRetryable(error)) {
throw error;
}
const delay = this.calculateDelay(attempt);
console.log(`Retry ${attempt}/${this.maxRetries} in ${delay}ms`);
await sleep(delay);
}
}
throw lastError;
}
private calculateDelay(attempt: number): number {
const exponential = Math.pow(2, attempt) * this.baseDelayMs;
return Math.min(exponential, this.maxDelayMs);
}
}
Usage Example
const retryManager = new RetryManager(3, 1000, 30000);
const result = await retryManager.execute(
async () => {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
},
(error) => error.status >= 500 || error.status === 429
);
For Specific Use Cases
API Calls
async function fetchWithRetry(url: string, maxRetries: number = 3): Promise<any> {
return retryWithBackoff(async () => {
const response = await fetch(url);
if (!response.ok) {
const error: any = new Error(`HTTP ${response.status}`);
error.code = response.status;
throw error;
}
return response.json();
}, maxRetries);
}
Database Operations
async function queryWithRetry(query: string, maxRetries: number = 3): Promise<any> {
return retryWithBackoff(async () => {
return await database.execute(query);
}, maxRetries);
}
File Operations
async function readFileWithRetry(filePath: string, maxRetries: number = 3): Promise<Buffer> {
return retryWithBackoff(async () => {
return await fs.promises.readFile(filePath);
}, maxRetries);
}
Logging Retry Attempts
async function retryWithLogging<T>(
operation: () => Promise<T>,
maxRetries: number = 3
): Promise<T> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await operation();
if (attempt > 1) {
console.log(`Operation succeeded on attempt ${attempt}`);
}
return result;
} catch (error) {
console.error(`Attempt ${attempt} failed:`, error.message);
if (attempt === maxRetries) {
console.error(`All ${maxRetries} attempts failed`);
throw error;
}
}
}
throw new Error('Retry failed');
}