18 min
Advanced Error Handling & Retry Logic
Learning Objectives
- Classify errors as retryable vs non-retryable
- Implement exponential backoff
- Add jitter to prevent thundering herd
- Use circuit breakers for failing services
Not All Errors Should Be Retried
Some errors are temporary (retry them):
•Network timeout
•Facilitator unavailable (503)
•Rate limit (429)
•Nonce conflict (EVM)
Some errors are permanent (don't retry):
•Insufficient funds
•Invalid private key
•No scheme registered
•404 Not Found
typescript
function shouldRetry(error: Error): boolean {
const message = error.message.toLowerCase();
// Network errors - retry
if (message.includes('timeout') ||
message.includes('econnrefused') ||
message.includes('network')) {
return true;
}
// Rate limit - retry
if (message.includes('rate limit') || message.includes('429')) {
return true;
}
// Configuration errors - don't retry
if (message.includes('no scheme registered') ||
message.includes('insufficient funds') ||
message.includes('invalid signature')) {
return false;
}
// Default: don't retry
return false;
}Exponential Backoff
Wait longer between each retry: 1s, 2s, 4s, 8s...
typescript
async function retryWithBackoff<T>(
operation: () => Promise<T>,
maxRetries = 3
): Promise<T> {
let delay = 1000; // Start with 1 second
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
// Don't retry if non-retryable
if (!shouldRetry(error as Error)) {
throw error;
}
// Last attempt failed
if (attempt === maxRetries) {
throw error;
}
console.log(`Retry ${attempt + 1}/${maxRetries} in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
// Double the delay for next attempt
delay *= 2;
}
}
throw new Error('Max retries reached');
}
// Usage
const data = await retryWithBackoff(async () => {
const response = await fetchWithPayment('https://api.example.com/data');
return response.json();
}, 3);Adding Jitter
Randomize delays slightly to prevent all clients retrying simultaneously:
typescript
function addJitter(delay: number): number {
// Add ±30% randomness
const jitter = delay * 0.3 * (Math.random() * 2 - 1);
return Math.max(0, delay + jitter);
}
// Instead of exactly 2000ms, get something like 1700ms or 2300ms
const delayWithJitter = addJitter(2000);Jitter prevents the "thundering herd" problem where thousands of clients retry at exactly the same time and overwhelm the server.
Circuit Breakers
Stop trying if a service keeps failing:
typescript
class CircuitBreaker {
private failures = 0;
private readonly threshold = 5;
private isOpen = false;
private resetTime = 0;
async execute<T>(operation: () => Promise<T>): Promise<T> {
// Circuit is open - reject immediately
if (this.isOpen && Date.now() < this.resetTime) {
throw new Error('Circuit breaker is OPEN');
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failures = 0;
this.isOpen = false;
}
private onFailure() {
this.failures++;
if (this.failures >= this.threshold) {
this.isOpen = true;
this.resetTime = Date.now() + 60000; // Wait 60s before trying again
console.warn('Circuit breaker opened - too many failures');
}
}
}
// Usage
const breaker = new CircuitBreaker();
try {
const data = await breaker.execute(async () => {
const response = await fetchWithPayment(url);
return response.json();
});
} catch (error) {
console.error('Request failed:', error.message);
}Build a resilient client with retry + circuit breaker
Combine everything: classify errors, exponential backoff with jitter, circuit breaker
Requirements:
Retries on network errors
Client retries when it gets timeout errors
Stops on config errors
Client does not retry on "no scheme registered"
Uses exponential backoff
Delays increase: 1s, 2s, 4s
Your Solution