PatternsAdvanced Error Handling
Circuit Breaker Pattern
Prevent cascading failures by detecting and isolating failing services
Circuit Breaker Pattern (Frontend)
The circuit breaker pattern prevents your app from repeatedly calling a failing service, reducing load and providing faster feedback to users.
How It Works
A circuit breaker has three states:
CLOSED (Normal)
↓ (failures exceed threshold)
OPEN (Blocking calls)
↓ (timeout expires)
HALF-OPEN (Testing)
↓ (success/failure)
CLOSED or OPENStates Explained
CLOSED (Default):
- Requests pass through normally
- Failures are counted
- If threshold exceeded → OPEN
OPEN (Failing):
- Requests fail immediately (fail-fast)
- No calls to failing service
- After timeout → HALF-OPEN
HALF-OPEN (Testing):
- Allow limited test requests
- Success → CLOSED
- Failure → OPEN
Basic Implementation
// lib/circuit-breaker.ts
export enum CircuitState {
CLOSED = 'CLOSED',
OPEN = 'OPEN',
HALF_OPEN = 'HALF_OPEN',
}
interface CircuitBreakerConfig {
failureThreshold: number; // Open after N failures
successThreshold: number; // Close after N successes in HALF_OPEN
timeout: number; // Time to wait before HALF_OPEN (ms)
halfOpenMaxCalls: number; // Max concurrent calls in HALF_OPEN
}
export class CircuitBreaker {
private state: CircuitState = CircuitState.CLOSED;
private failureCount = 0;
private successCount = 0;
private nextAttempt: number = Date.now();
private halfOpenCalls = 0;
constructor(
private name: string,
private config: CircuitBreakerConfig = {
failureThreshold: 5,
successThreshold: 2,
timeout: 60000, // 1 minute
halfOpenMaxCalls: 3,
}
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
// Check if circuit should transition to HALF_OPEN
if (this.state === CircuitState.OPEN) {
if (Date.now() < this.nextAttempt) {
throw new CircuitBreakerError(`Circuit breaker "${this.name}" is OPEN`);
}
this.state = CircuitState.HALF_OPEN;
this.halfOpenCalls = 0;
console.log(`[Circuit Breaker: ${this.name}] → HALF_OPEN`);
}
// Limit concurrent calls in HALF_OPEN
if (this.state === CircuitState.HALF_OPEN) {
if (this.halfOpenCalls >= this.config.halfOpenMaxCalls) {
throw new CircuitBreakerError(`Circuit breaker "${this.name}" is testing (HALF_OPEN)`);
}
this.halfOpenCalls++;
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failureCount = 0;
if (this.state === CircuitState.HALF_OPEN) {
this.successCount++;
if (this.successCount >= this.config.successThreshold) {
this.state = CircuitState.CLOSED;
this.successCount = 0;
console.log(`[Circuit Breaker: ${this.name}] → CLOSED`);
}
}
}
private onFailure() {
this.failureCount++;
this.successCount = 0;
if (
this.state === CircuitState.CLOSED &&
this.failureCount >= this.config.failureThreshold
) {
this.state = CircuitState.OPEN;
this.nextAttempt = Date.now() + this.config.timeout;
console.log(`[Circuit Breaker: ${this.name}] → OPEN`);
}
if (this.state === CircuitState.HALF_OPEN) {
this.state = CircuitState.OPEN;
this.nextAttempt = Date.now() + this.config.timeout;
console.log(`[Circuit Breaker: ${this.name}] → OPEN (from HALF_OPEN)`);
}
}
getState() {
return {
state: this.state,
failureCount: this.failureCount,
successCount: this.successCount,
nextAttempt: new Date(this.nextAttempt),
};
}
reset() {
this.state = CircuitState.CLOSED;
this.failureCount = 0;
this.successCount = 0;
this.halfOpenCalls = 0;
console.log(`[Circuit Breaker: ${this.name}] → RESET`);
}
}
export class CircuitBreakerError extends Error {
constructor(message: string) {
super(message);
this.name = 'CircuitBreakerError';
}
}Usage with API Calls
// lib/api-client.ts
import { CircuitBreaker } from './circuit-breaker';
// Create circuit breakers for different services
const breakers = {
recommendations: new CircuitBreaker('recommendations', {
failureThreshold: 3,
timeout: 30000, // 30s
}),
analytics: new CircuitBreaker('analytics', {
failureThreshold: 5,
timeout: 60000, // 1min
}),
payments: new CircuitBreaker('payments', {
failureThreshold: 2, // More sensitive
timeout: 120000, // 2min
}),
};
export async function fetchRecommendations() {
return breakers.recommendations.execute(async () => {
const response = await fetch('/api/recommendations');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
});
}
// Usage in component
function Recommendations() {
const { data, error } = useQuery('recommendations', fetchRecommendations);
if (error instanceof CircuitBreakerError) {
return (
<div className="service-unavailable">
<p>Recommendations are temporarily unavailable.</p>
<p>We're working on it. Please try again later.</p>
</div>
);
}
if (error) {
return <ErrorUI error={error} />;
}
return <RecommendationsList data={data} />;
}React Hook Integration
// hooks/useCircuitBreaker.ts
import { useState, useEffect } from 'react';
import { CircuitBreaker, CircuitState } from '@/lib/circuit-breaker';
export function useCircuitBreaker<T>(
name: string,
fn: () => Promise<T>,
options?: {
enabled?: boolean;
onStateChange?: (state: CircuitState) => void;
}
) {
const [breaker] = useState(() => new CircuitBreaker(name));
const [state, setState] = useState(breaker.getState());
// Monitor state changes
useEffect(() => {
const interval = setInterval(() => {
const currentState = breaker.getState();
setState(currentState);
if (options?.onStateChange && currentState.state !== state.state) {
options.onStateChange(currentState.state);
}
}, 1000);
return () => clearInterval(interval);
}, [breaker, options, state.state]);
const execute = async () => {
if (!options?.enabled) {
return fn();
}
return breaker.execute(fn);
};
return {
execute,
state: state.state,
stats: state,
reset: () => breaker.reset(),
};
}
// Usage
function DataComponent() {
const { execute, state, reset } = useCircuitBreaker(
'data-service',
() => fetchData(),
{
enabled: true,
onStateChange: (newState) => {
if (newState === CircuitState.OPEN) {
toast.error('Service temporarily unavailable');
}
},
}
);
const { data, error } = useQuery('data', execute);
return (
<div>
{state === CircuitState.OPEN && (
<Banner type="error">
Service is unavailable. Retrying automatically...
</Banner>
)}
{state === CircuitState.HALF_OPEN && (
<Banner type="warning">
Testing service connection...
</Banner>
)}
{/* ... render data ... */}
</div>
);
}Multiple Service Management
// lib/circuit-breaker-manager.ts
import { CircuitBreaker, CircuitState } from './circuit-breaker';
class CircuitBreakerManager {
private breakers = new Map<string, CircuitBreaker>();
getOrCreate(name: string, config?: any): CircuitBreaker {
if (!this.breakers.has(name)) {
this.breakers.set(name, new CircuitBreaker(name, config));
}
return this.breakers.get(name)!;
}
getStatus() {
const status: Record<string, any> = {};
this.breakers.forEach((breaker, name) => {
status[name] = breaker.getState();
});
return status;
}
getHealthScore(): number {
const states = Array.from(this.breakers.values()).map(b => b.getState().state);
if (states.length === 0) return 1;
const closed = states.filter(s => s === CircuitState.CLOSED).length;
return closed / states.length;
}
resetAll() {
this.breakers.forEach(breaker => breaker.reset());
}
}
export const circuitBreakerManager = new CircuitBreakerManager();
// Usage
const breaker = circuitBreakerManager.getOrCreate('recommendations');
await breaker.execute(() => fetchRecommendations());Visual Indicators
// components/CircuitBreakerStatus.tsx
'use client';
import { useEffect, useState } from 'react';
import { circuitBreakerManager } from '@/lib/circuit-breaker-manager';
import { CircuitState } from '@/lib/circuit-breaker';
export function CircuitBreakerStatus() {
const [status, setStatus] = useState(circuitBreakerManager.getStatus());
const [health, setHealth] = useState(circuitBreakerManager.getHealthScore());
useEffect(() => {
const interval = setInterval(() => {
setStatus(circuitBreakerManager.getStatus());
setHealth(circuitBreakerManager.getHealthScore());
}, 1000);
return () => clearInterval(interval);
}, []);
// Only show in dev or for admins
if (process.env.NODE_ENV === 'production' && !isAdmin()) {
return null;
}
return (
<div className="circuit-breaker-status">
<h3>Services Health: {(health * 100).toFixed(0)}%</h3>
<div className="breakers-list">
{Object.entries(status).map(([name, state]: [string, any]) => (
<div key={name} className="breaker-item">
<span className="breaker-name">{name}</span>
<span className={`breaker-state state-${state.state.toLowerCase()}`}>
{state.state}
</span>
{state.state === CircuitState.OPEN && (
<span className="retry-time">
Retry in {Math.round((state.nextAttempt.getTime() - Date.now()) / 1000)}s
</span>
)}
</div>
))}
</div>
</div>
);
}Advanced Patterns
Fallback with Circuit Breaker
// Combine circuit breaker with fallback data
async function getDataWithFallback() {
try {
return await breaker.execute(() => fetchData());
} catch (error) {
if (error instanceof CircuitBreakerError) {
// Circuit is open, use fallback immediately
console.log('Using fallback data (circuit open)');
return getFallbackData();
}
// Other errors, still try fallback
console.log('Using fallback data (fetch failed)');
return getFallbackData();
}
}Cascading Circuit Breakers
// Primary service with fallback service
async function getRecommendations() {
try {
// Try primary service
return await primaryBreaker.execute(() => fetchFromPrimary());
} catch (error) {
// Primary failed, try fallback service
try {
return await fallbackBreaker.execute(() => fetchFromFallback());
} catch (fallbackError) {
// Both failed, use static recommendations
return getStaticRecommendations();
}
}
}Metrics Collection
// Track circuit breaker metrics
class CircuitBreakerWithMetrics extends CircuitBreaker {
private metrics = {
totalCalls: 0,
successfulCalls: 0,
failedCalls: 0,
rejectedCalls: 0, // Rejected by circuit
stateChanges: [] as Array<{ from: CircuitState; to: CircuitState; timestamp: Date }>,
};
async execute<T>(fn: () => Promise<T>): Promise<T> {
this.metrics.totalCalls++;
try {
const result = await super.execute(fn);
this.metrics.successfulCalls++;
return result;
} catch (error) {
if (error instanceof CircuitBreakerError) {
this.metrics.rejectedCalls++;
} else {
this.metrics.failedCalls++;
}
throw error;
}
}
getMetrics() {
return {
...this.metrics,
successRate: this.metrics.successfulCalls / this.metrics.totalCalls,
failureRate: this.metrics.failedCalls / this.metrics.totalCalls,
rejectionRate: this.metrics.rejectedCalls / this.metrics.totalCalls,
};
}
}Testing Circuit Breaker
// __tests__/circuit-breaker.test.ts
import { CircuitBreaker, CircuitState } from '../circuit-breaker';
describe('CircuitBreaker', () => {
it('opens after threshold failures', async () => {
const breaker = new CircuitBreaker('test', {
failureThreshold: 3,
timeout: 1000,
});
const failingFn = () => Promise.reject(new Error('Failed'));
// Fail 3 times
for (let i = 0; i < 3; i++) {
await expect(breaker.execute(failingFn)).rejects.toThrow('Failed');
}
// Circuit should be open now
expect(breaker.getState().state).toBe(CircuitState.OPEN);
// Next call should be rejected immediately
await expect(breaker.execute(failingFn)).rejects.toThrow('Circuit breaker');
});
it('transitions to half-open after timeout', async () => {
const breaker = new CircuitBreaker('test', {
failureThreshold: 2,
timeout: 100, // 100ms
});
const failingFn = () => Promise.reject(new Error('Failed'));
// Open the circuit
await expect(breaker.execute(failingFn)).rejects.toThrow();
await expect(breaker.execute(failingFn)).rejects.toThrow();
expect(breaker.getState().state).toBe(CircuitState.OPEN);
// Wait for timeout
await new Promise(resolve => setTimeout(resolve, 150));
// Next call should transition to HALF_OPEN
const successFn = () => Promise.resolve('success');
await breaker.execute(successFn);
// Should still be HALF_OPEN (needs more successes)
expect(breaker.getState().state).toBe(CircuitState.HALF_OPEN);
});
});When to Use Circuit Breaker
✅ Use circuit breaker for:
- External API calls
- Third-party services
- Microservices communication
- Non-critical features (recommendations, analytics)
- Services with known reliability issues
❌ Don't use for:
- Critical services (auth, payments)
- Database queries (use connection pools instead)
- Client-side operations
- Services with SLA guarantees
Best Practices
- Different Thresholds: Critical services = stricter, optional = lenient
- Monitor State Changes: Alert when circuits open
- Provide Fallbacks: Always have Plan B
- Test Failure Scenarios: Simulate service outages
- Log State Transitions: Track when/why circuits open
- Progressive Timeout: Longer timeouts after repeated failures
- User Communication: Show when services are degraded
Common Mistakes
❌ Same threshold for all services: Different criticality
✅ Tune per service
❌ No fallback: Users see errors
✅ Always provide fallback
❌ Too sensitive: Opens too easily
✅ Balance sensitivity
❌ No monitoring: Don't know when circuits open
✅ Alert on state changes
Circuit Breaker Configuration Guide
| Service Type | Failure Threshold | Timeout | Half-Open Calls |
|---|---|---|---|
| Critical | 1-2 | 2-5 min | 1 |
| Important | 3-5 | 1-2 min | 2-3 |
| Optional | 5-10 | 30-60s | 3-5 |
| Analytics | 10+ | 10-30s | 5-10 |
Circuit breakers are your first line of defense against cascading failures—use them to isolate problems before they spread.