Client Path
Lesson 8 of 8
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