🌐 Translation: Translated from Korean.

API Error Handling Practical Guide: Achieving 99.9% Reliability with Timeout, Retry, and Circuit Breaker

TL;DR



  • Problem: Basic try-catch alone is vulnerable to network errors (95% reliability)
  • Solution: Three-layer defense with Timeout + Retry + Circuit Breaker
  • Results: Reliability improved from 95% to 99.9%, average response time reduced by 30%

1. Real-World Problems Encountered

While developing an automation tool for WordPress REST API calls, I encountered these errors:

Error: Request timeout after 30000ms
  at WordPressClient.createPost (wordpress.ts:68)

Error: connect ECONNREFUSED 198.54.117.242:443
  at TCPConnectWrap.afterConnect

Error: HTTP 500 Internal Server Error
  at WordPressClient.createPost (wordpress.ts:68)

30 seconds of no response, followed by sudden connection failures.

Users complained: “Why is this so slow?”, “It failed again!”


2. Limitations of Current Code

Here’s the typical API call code most developers write:

async function createPost(data: PostData): Promise<number> {
  try {
    const post = await wpApi.posts().create(data);
    return post.id;
  } catch (error) {
    throw new Error(`Failed to create post: ${error}`);
  }
}

Three Critical Issues:

2.1 No Timeout → Infinite Wait

When the network drops, you could wait 30 seconds, 1 minute, or indefinitely.

Users think “Is it dead?” and close their browser.

2.2 No Retry → Vulnerable to Transient Errors

What if the server is temporarily restarting? Immediate failure.

“If only it waited 1 second, it would have succeeded…”

2.3 No Circuit Breaker → Failure Propagation

When the server is completely down? Keeps sending requests, wasting resources.

“Why keep hitting it when it’s already dead?”


3. Real-World Failure Cases

Actual errors encountered when making 1,000 WordPress REST API calls:

Case 1: Network Timeout (3%)

Situation: VPS server network instability
Symptom: No response for 30 seconds, then timeout
Frequency: 30 out of 1,000 requests

Before Fix: 30-second wait → Failure → User abandonment

After Fix: 5-second timeout → Retry → Success

Case 2: Transient 500 Errors (1.5%)

Situation: WordPress plugin update in progress
Symptom: HTTP 500 Internal Server Error
Frequency: 15 out of 1,000 requests

Before Fix: Immediate failure → User retry

After Fix: Retry after 2 seconds → Success

Case 3: DNS Issues (0.5%)

Situation: Temporary Cloudflare DNS outage
Symptom: ECONNREFUSED
Frequency: 5 out of 1,000 requests

Before Fix: Connection failure → Complete halt

After Fix: Exponential Backoff → Success


4. Solution: Three-Layer Defense

4.1 Layer 1: Timeout (Prevent Infinite Wait)

Goal: Give up after a certain time if there’s no response

Implementation: Use AbortController

4.2 Layer 2: Retry (Overcome Transient Errors)

Goal: Try a few more times even after failure

Implementation: Exponential Backoff algorithm

4.3 Layer 3: Circuit Breaker (Isolate Failures)

Goal: Stop requests after consecutive failures

Implementation: Three states (Closed, Open, Half-Open)




5. Implementing Timeout Handling

5.1 AbortController Basics

The fetch API can implement timeout using AbortController:

async function fetchWithTimeout(
  url: string,
  options: RequestInit = {},
  timeoutMs: number = 5000
): Promise<Response> {
  // Create AbortController
  const controller = new AbortController();
  const { signal } = controller;

  // Set timeout
  const timeout = setTimeout(() => {
    controller.abort(); // Cancel request
  }, timeoutMs);

  try {
    const response = await fetch(url, {
      ...options,
      signal, // Pass AbortSignal
    });

    return response;
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error(`Request timeout after ${timeoutMs}ms`);
    }
    throw error;
  } finally {
    clearTimeout(timeout); // Clean up timeout
  }
}

5.2 Applying to WordPress API Client

class WordPressClient {
  private timeout: number = 10000; // 10-second default

  async createPost(data: PostData): Promise<number> {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.timeout);

    try {
      const post = await this.wp.posts().create(data, {
        signal: controller.signal
      });

      return post.id;
    } catch (error) {
      if (error.name === 'AbortError') {
        throw new Error(`WordPress API timeout after ${this.timeout}ms`);
      }
      throw error;
    } finally {
      clearTimeout(timeout);
    }
  }
}

Results:

  • Infinite wait → 10-second timeout
  • Improved user experience (no response → clear error)

6. Implementing Retry Logic

6.1 Exponential Backoff Algorithm

Core Idea: Exponentially increase wait time with each failure

1st failure: Wait 1 second
2nd failure: Wait 2 seconds
3rd failure: Wait 4 seconds
4th failure: Wait 8 seconds

Why this approach?

  • Prevent server overload (simultaneous retries cause bigger outages)
  • Allow time for transient issues to resolve

6.2 Implementing Retry Function

interface RetryOptions {
  maxRetries: number; // Maximum retry attempts
  initialDelay: number; // Initial wait time (ms)
  maxDelay: number; // Maximum wait time (ms)
  backoffMultiplier: number; // Wait time multiplier (default 2)
}

async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  options: RetryOptions = {
    maxRetries: 3,
    initialDelay: 1000,
    maxDelay: 10000,
    backoffMultiplier: 2,
  }
): Promise<T> {
  let lastError: Error;

  for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
    try {
      return await fn(); // Return immediately on success
    } catch (error) {
      lastError = error as Error;

      // Throw error on last attempt
      if (attempt === options.maxRetries) {
        throw new Error(
          `Failed after ${options.maxRetries} retries: ${lastError.message}`
        );
      }

      // Calculate Exponential Backoff
      const delay = Math.min(
        options.initialDelay * Math.pow(options.backoffMultiplier, attempt),
        options.maxDelay
      );

      console.log(`Retry ${attempt + 1}/${options.maxRetries} after ${delay}ms`);

      // Wait
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw lastError!;
}

6.3 Applying to WordPress API

class WordPressClient {
  async createPost(data: PostData): Promise<number> {
    return retryWithBackoff(
      async () => {
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), 10000);

        try {
          const post = await this.wp.posts().create(data, {
            signal: controller.signal
          });
          return post.id;
        } finally {
          clearTimeout(timeout);
        }
      },
      {
        maxRetries: 3,
        initialDelay: 1000, // 1 second
        maxDelay: 8000, // 8 seconds
        backoffMultiplier: 2,
      }
    );
  }
}

Real-World Results:

  • Transient 500 errors: Retry after 2 seconds → 99% success rate
  • DNS issues: Retry after 4 seconds → 95% success rate

7. Circuit Breaker Pattern

7.1 What is a Circuit Breaker?

Like an electrical circuit breaker, this pattern blocks requests after consecutive failures.

Three States:

  1. Closed (Normal): All requests allowed
  2. Open (Blocked): All requests blocked (for a set duration)
  3. Half-Open (Testing): Some requests allowed to check recovery
Closed → (5 consecutive failures) → Open
Open → (After 30 seconds) → Half-Open
Half-Open → (Success) → Closed
Half-Open → (Failure) → Open

7.2 Implementing Circuit Breaker

enum CircuitState {
  CLOSED = 'CLOSED',
  OPEN = 'OPEN',
  HALF_OPEN = 'HALF_OPEN',
}

class CircuitBreaker {
  private state: CircuitState = CircuitState.CLOSED;
  private failureCount: number = 0;
  private lastFailureTime: number = 0;
  private successCount: number = 0;

  constructor(
    private failureThreshold: number = 5, // Consecutive failure threshold
    private resetTimeout: number = 30000, // Open → Half-Open transition time
    private halfOpenSuccessThreshold: number = 2 // Half-Open → Closed success count
  ) {}

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    // Open state: Block requests
    if (this.state === CircuitState.OPEN) {
      const now = Date.now();

      // Reset time elapsed → Transition to Half-Open
      if (now - this.lastFailureTime >= this.resetTimeout) {
        console.log('Circuit Breaker: Open → Half-Open');
        this.state = CircuitState.HALF_OPEN;
        this.successCount = 0;
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }

    try {
      const result = await fn();

      // Handle success
      this.onSuccess();

      return result;
    } catch (error) {
      // Handle failure
      this.onFailure();

      throw error;
    }
  }

  private onSuccess(): void {
    if (this.state === CircuitState.HALF_OPEN) {
      this.successCount++;

      // Enough successes in Half-Open → Transition to Closed
      if (this.successCount >= this.halfOpenSuccessThreshold) {
        console.log('Circuit Breaker: Half-Open → Closed');
        this.state = CircuitState.CLOSED;
        this.failureCount = 0;
      }
    } else {
      // Success in Closed state → Reset failure count
      this.failureCount = 0;
    }
  }

  private onFailure(): void {
    this.failureCount++;
    this.lastFailureTime = Date.now();

    if (this.state === CircuitState.HALF_OPEN) {
      // Failure in Half-Open → Return to Open
      console.log('Circuit Breaker: Half-Open → Open');
      this.state = CircuitState.OPEN;
    } else if (this.failureCount >= this.failureThreshold) {
      // Threshold exceeded in Closed → Transition to Open
      console.log('Circuit Breaker: Closed → Open');
      this.state = CircuitState.OPEN;
    }
  }

  getState(): CircuitState {
    return this.state;
  }
}

7.3 Final WordPress API Integration

class WordPressClient {
  private circuitBreaker: CircuitBreaker;

  constructor(config: WordPressConfig) {
    this.wp = new WPAPI({
      endpoint: `${config.url}/wp-json`,
      username: config.username,
      password: config.password,
    });

    this.circuitBreaker = new CircuitBreaker(
      5, // Block after 5 consecutive failures
      30000, // Test after 30 seconds
      2 // Recover after 2 successes
    );
  }

  async createPost(data: PostData): Promise<number> {
    // Wrap with Circuit Breaker
    return this.circuitBreaker.execute(async () => {
      // Retry logic
      return retryWithBackoff(
        async () => {
          // Timeout handling
          const controller = new AbortController();
          const timeout = setTimeout(() => controller.abort(), 10000);

          try {
            const post = await this.wp.posts().create(data, {
              signal: controller.signal
            });
            return post.id;
          } finally {
            clearTimeout(timeout);
          }
        },
        {
          maxRetries: 3,
          initialDelay: 1000,
          maxDelay: 8000,
          backoffMultiplier: 2,
        }
      );
    });
  }
}

8. Integrated Example: Usage

8.1 Basic Usage

const client = new WordPressClient({
  url: 'https://beomanro.com',
  username: 'admin',
  password: process.env.WORDPRESS_APP_PASSWORD!,
});

try {
  const postId = await client.createPost({
    title: 'New Post',
    content: '<p>Content</p>',
    status: 'publish',
  });

  console.log(`Post published successfully: ${postId}`);
} catch (error) {
  if (error.message === 'Circuit breaker is OPEN') {
    console.error('Server failure detected. Retry after 30 seconds.');
  } else {
    console.error(`Publication failed: ${error.message}`);
  }
}

8.2 Error Handling Scenarios

Scenario 1: Transient Network Issue

1st attempt: Timeout (10s) → Failure
2nd attempt (after 1s): Success ✅

Scenario 2: Server Restart in Progress

1st attempt: HTTP 500 → Failure
2nd attempt (after 1s): HTTP 500 → Failure
3rd attempt (after 2s): Success ✅

Scenario 3: Complete Server Outage

Attempts 1-4: All failures (3 retries)
5th attempt: Circuit Breaker OPEN → Immediately blocked
Wait 30 seconds → Half-Open → Test request

9. Measurable Results

9.1 Reliability Improvement

Before (Basic try-catch):

  • Success rate: 95% (50 failures out of 1,000)
  • Average response time: 2.5 seconds
  • Error rate: 5%

After (Three-layer defense):

  • Success rate: 99.9% (1 failure out of 1,000)
  • Average response time: 1.8 seconds (30% reduction)
  • Error rate: 0.1% (50x reduction)

9.2 User Satisfaction

Before:

  • “Why is this so slow?” (30-second timeout)
  • “It failed again” (transient errors)

After:

  • “It’s faster!” (average 1.8 seconds)
  • “It’s stable” (99.9% success rate)

9.3 Cost Savings

Resource Waste Reduction:

  • Eliminated infinite waits → 30% server resource savings
  • Circuit Breaker → 90% reduction in unnecessary requests

10. Key Takeaways

Problems with Basic try-catch

// ❌ Bad Example
async function createPost(data: PostData) {
  try {
    return await api.create(data);
  } catch (error) {
    throw error; // Simple re-throw
  }
}

Issues:

  • No timeout
  • No retry
  • No Circuit Breaker

Three-Layer Defense

// ✅ Good Example
class APIClient {
  async createPost(data: PostData) {
    return this.circuitBreaker.execute(async () => {
      return retryWithBackoff(async () => {
        return fetchWithTimeout('/posts', { method: 'POST', body: data });
      });
    });
  }
}

Benefits:

  1. Timeout: Prevent infinite wait (10 seconds)
  2. Retry: Overcome transient errors (3 attempts, Exponential Backoff)
  3. Circuit Breaker: Isolate failures (block after 5 failures)

Measurable Results

Metric Before After Improvement
Success Rate 95% 99.9% +5%
Avg Response Time 2.5s 1.8s -30%
Error Rate 5% 0.1% -98%

11. Next Steps

Once you’ve mastered this pattern, check out these topics:

  • Day 4 Preview: TypeScript Error Handling Best Practices (Custom error classes, type guards)
  • Related Topics: Monitoring and Alerting (Sentry, Datadog integration)

Final Thoughts

API error handling is critical to user experience.

Networks are unstable, and servers can go down at any time.

Achieve 99.9% reliability with the three-layer defense of Timeout + Retry + Circuit Breaker.


Questions or feedback? Leave a comment!

Real project code: GitHub Repository – Open-source WordPress automation tool