typescript 123 lines · 3 tabs

Circuit breaker wrapper for flaky third-party APIs

Shared by codesnips Jan 2026
3 tabs
export type CircuitState = "closed" | "open" | "half-open";

export interface CircuitBreakerOptions {
  failureThreshold: number;
  resetTimeout: number; // ms to wait in "open" before probing
  onStateChange?: (from: CircuitState, to: CircuitState) => void;
}

export class CircuitOpenError extends Error {
  constructor(public readonly retryAfterMs: number) {
    super(`Circuit is open; retry in ${retryAfterMs}ms`);
    this.name = "CircuitOpenError";
  }
}
3 files · typescript Explain with highlit

A circuit breaker protects a service from a downstream dependency that has started failing or timing out. Rather than sending every request into a black hole and exhausting connection pools or thread budgets, the breaker watches the recent failure rate and, once it crosses a threshold, "opens" — short-circuiting further calls with an immediate error until the dependency has had time to recover. This snippet shows a small, self-contained implementation and how it wraps a real HTTP client.

The CircuitBreaker tab holds the state machine. It tracks three states — closed (traffic flows), open (calls are rejected fast), and half-open (a single probe is allowed through to test recovery). execute is the entry point: when the breaker is open, it checks whether resetTimeout has elapsed and either transitions to half-open or throws a CircuitOpenError without ever touching the network. Successes and failures feed onSuccess/onFailure, which use a rolling failures count and a failureThreshold to decide when to trip. A single failure in half-open re-opens the circuit, which is deliberate: recovery should be proven, not assumed.

The key trade-off is the sliding threshold versus a rate-based window. This version counts consecutive-ish failures for simplicity, which is easy to reason about but can be noisy under bursty traffic; a production system often layers in a percentage over a time window. Note also that the breaker only guards against the failures it can see — a timeout must be enforced by the caller, which is why the client below wraps fetch in an AbortController.

The PaymentApiClient tab shows the realistic usage. Each public method routes its network call through breaker.execute, so retries and timeouts live in one place. withTimeout races the request against an abort signal, and non-2xx responses are thrown so the breaker counts them as failures rather than silently succeeding. The types tab defines the shared options and the typed error so callers can distinguish a genuinely-open circuit from a normal request failure and, for example, serve cached data instead. This pattern is worth reaching for whenever a dependency is flaky enough that failing fast is cheaper than waiting on it.


Related snips

Share this code

Here's the card — post it anywhere.

Circuit breaker wrapper for flaky third-party APIs — share card
Link copied