Package Exports
- @apiratorjs/circuit-breaker
- @apiratorjs/circuit-breaker/dist/src/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@apiratorjs/circuit-breaker) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
@apiratorjs/circuit-breaker
A robust and lightweight TypeScript circuit breaker implementation for Node.js applications. Provides fault tolerance and stability by preventing cascading failures in distributed systems with configurable thresholds and automatic recovery.
Note: Requires Node.js version >=16.4.0
What is a Circuit Breaker and Why Use It?
A Circuit Breaker is a design pattern used in distributed systems to provide fault tolerance and prevent cascading failures. Just like an electrical circuit breaker that protects your home's electrical system from overload, a software circuit breaker protects your application from failing services.
How It Works
The circuit breaker monitors calls to external services and tracks failures. It has three states:
- 🟢 CLOSED: Normal operation - requests pass through and are monitored
- 🔴 OPEN: Failure threshold exceeded - requests fail fast without calling the service
- 🟡 HALF_OPEN: Testing phase - allows limited requests to check if service has recovered
Why You Need It
Without a Circuit Breaker:
Service A → Service B (failing) → Timeout after 30s → Retry → Another 30s timeout → Cascade failureWith a Circuit Breaker:
Service A → Circuit Breaker → Service B (failing) → Fast fail after threshold → System remains stableKey Benefits
- Fast Failure: Stop wasting time on calls to failing services
- System Stability: Prevent one failing service from bringing down your entire system
- Automatic Recovery: Automatically retry when services become healthy again
- Observability: Get insights into service health and failure patterns
- Performance: Reduce resource consumption and improve response times
Installation
npm install @apiratorjs/circuit-breakeryarn add @apiratorjs/circuit-breakerpnpm add @apiratorjs/circuit-breakerQuick Start
import { CircuitBreaker, CircuitOpenError } from '@apiratorjs/circuit-breaker';
// Define your service call function
async function callExternalService(data: any) {
// Your external service call here
const response = await fetch('https://api.example.com/data', {
method: 'POST',
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`Service responded with ${response.status}`);
}
return response.json();
}
// Create a circuit breaker with your operation and settings
const circuitBreaker = new CircuitBreaker(callExternalService, {
failureThreshold: 5, // Open circuit after 5 failures
durationOfBreakInMs: 60000, // Keep circuit open for 60 seconds
successThreshold: 2 // Close circuit after 2 successful calls in half-open state
});
// Use it in your application
try {
const result = await circuitBreaker.execute({ id: 123 });
console.log('Success:', result);
} catch (error) {
if (error instanceof CircuitOpenError) {
console.error('Circuit is open - service temporarily unavailable');
} else {
console.error('Service call failed:', error.message);
}
}Interface and options
ICircuitBreakerOptions
Configuration options for creating a circuit breaker instance:
interface ICircuitBreakerOptions {
failureThreshold: number; // Number of failures before opening the circuit
durationOfBreakInMs: number; // How long to keep circuit open (milliseconds)
successThreshold: number; // Successful calls needed to close circuit from half-open
fallback?: (...args: any[]) => any; // Fallback function when circuit is open
errorFilter?: (error: Error) => boolean; // Custom error filtering logic
}Options Details
failureThreshold(required): Number of consecutive failures that will trigger the circuit to opendurationOfBreakInMs(required): Duration in milliseconds to keep the circuit open before attempting recoverysuccessThreshold(required): Number of successful calls needed in half-open state to close the circuitfallback(optional): Function to execute when circuit is open, receives same arguments as the operationerrorFilter(optional): Function to determine if an error should count as a failure (return true to count, false to ignore)
Circuit Breaker States
enum ECircuitBreakerState {
CLOSED = "closed", // Normal operation, calls pass through
OPEN = "open", // Circuit is open, calls are rejected immediately
HALF_OPEN = "half_open" // Testing recovery, limited calls allowed
}Error Handling
The circuit breaker throws specific error types that you can catch and handle appropriately:
Error Types
CircuitOpenError
Thrown when the circuit breaker is in the OPEN state and prevents execution of the wrapped function.
Properties:
cause?: TErrorLike- The original error that caused the circuit to opendurationTillNextAttemptInMs: number- Time in milliseconds until the circuit breaker will attempt recovery
import { CircuitBreaker, CircuitOpenError } from '@apiratorjs/circuit-breaker';
const circuitBreaker = new CircuitBreaker(riskyOperation, {
failureThreshold: 3,
durationOfBreakInMs: 30000,
successThreshold: 2
});
try {
await circuitBreaker.execute();
} catch (error) {
if (error instanceof CircuitOpenError) {
console.log('Circuit is open, service is temporarily unavailable');
console.log('Original cause:', error.cause?.message);
console.log(`Next attempt in: ${error.durationTillNextAttemptInMs}ms`);
// You can use this to implement retry logic or user feedback
const nextAttemptTime = new Date(Date.now() + error.durationTillNextAttemptInMs);
console.log(`Service will be available again at: ${nextAttemptTime.toISOString()}`);
}
}CircuitArgumentError
Thrown when invalid configuration options are provided to the circuit breaker constructor.
try {
// This will throw CircuitArgumentError
const circuitBreaker = new CircuitBreaker(myFunction, {
failureThreshold: 0, // Invalid: must be > 0
durationOfBreakInMs: 30000,
successThreshold: 2
});
} catch (error) {
if (error instanceof CircuitArgumentError) {
console.log('Invalid configuration:', error.message);
}
}CircuitBreakerError
Base class for all circuit breaker errors. Contains additional error information:
import { CircuitBreakerError } from '@apiratorjs/circuit-breaker';
try {
await circuitBreaker.execute();
} catch (error) {
if (error instanceof CircuitBreakerError) {
console.log('Circuit breaker error:', error.toJSON());
// Output includes: name, message, cause (if available)
}
}State Change Monitoring
You can subscribe to state changes to monitor your circuit breaker's behavior and implement custom logging, metrics, or alerting.
onStateChange Method
The onStateChange method allows you to register a callback that will be called whenever the circuit breaker changes state. The callback receives a state transition object with both the previous and new states, plus an optional error when transitioning to OPEN:
import { CircuitBreaker, ECircuitBreakerState } from '@apiratorjs/circuit-breaker';
const circuitBreaker = new CircuitBreaker(riskyOperation, {
failureThreshold: 3,
durationOfBreakInMs: 30000,
successThreshold: 2
});
// Subscribe to state changes
circuitBreaker.onStateChange((stateTransition, error) => {
console.log(`Circuit breaker: ${stateTransition.previousState} → ${stateTransition.newState}`);
// Handle state transitions
if (stateTransition.newState === ECircuitBreakerState.OPEN) {
console.log('⚠️ Circuit opened due to failures');
if (error) {
console.log('Last error:', error.message);
}
// Send alert, update metrics, etc.
} else if (stateTransition.newState === ECircuitBreakerState.HALF_OPEN) {
console.log('🔄 Circuit is testing recovery');
// Log recovery attempt
} else if (stateTransition.newState === ECircuitBreakerState.CLOSED) {
console.log('✅ Circuit closed - service is healthy');
// Log successful recovery
}
});Callback Signature:
type TCircuitBreakerStateChangeCallback = (
stateTransition: ICircuitBreakerStateTransition,
error?: Error // Present only when transitioning to OPEN state
) => void;
interface ICircuitBreakerStateTransition {
previousState: ECircuitBreakerState; // The state before transition
newState: ECircuitBreakerState; // The new current state
}Advanced State Monitoring Example
class CircuitBreakerMonitor {
private metrics = {
stateChanges: 0,
totalFailures: 0,
recoveryAttempts: 0,
transitions: [] as Array<{
from: ECircuitBreakerState;
to: ECircuitBreakerState;
timestamp: Date;
error?: string;
}>
};
constructor(private circuitBreaker: CircuitBreaker) {
this.setupMonitoring();
}
private setupMonitoring() {
this.circuitBreaker.onStateChange((stateTransition, error) => {
this.metrics.stateChanges++;
// Track all transitions
this.metrics.transitions.push({
from: stateTransition.previousState,
to: stateTransition.newState,
timestamp: new Date(),
error: error?.message
});
// Handle specific transitions
if (stateTransition.newState === ECircuitBreakerState.OPEN) {
this.metrics.totalFailures++;
this.onCircuitOpened(stateTransition.previousState, error);
} else if (stateTransition.newState === ECircuitBreakerState.HALF_OPEN) {
this.metrics.recoveryAttempts++;
this.onRecoveryAttempt(stateTransition.previousState);
} else if (stateTransition.newState === ECircuitBreakerState.CLOSED) {
this.onCircuitClosed(stateTransition.previousState);
}
});
}
private onCircuitOpened(from: ECircuitBreakerState, error?: Error) {
console.log(`🚨 ALERT: Circuit breaker opened (${from} → OPEN)`);
console.log('Error details:', error?.message);
// Send to monitoring system
// this.sendAlert('circuit_breaker_opened', {
// from,
// error: error?.message
// });
}
private onRecoveryAttempt(from: ECircuitBreakerState) {
console.log(`🔄 Circuit breaker attempting recovery (${from} → HALF_OPEN)`);
// Log recovery attempt
// this.logMetric('circuit_breaker_recovery_attempt');
}
private onCircuitClosed(from: ECircuitBreakerState) {
console.log(`✅ Circuit breaker recovered (${from} → CLOSED)`);
// Log successful recovery
// this.logMetric('circuit_breaker_recovered');
}
public getMetrics() {
return {
...this.metrics,
currentState: this.circuitBreaker.state
};
}
}
// Usage
const monitor = new CircuitBreakerMonitor(circuitBreaker);
// Check metrics after some operations
console.log(monitor.getMetrics());
// Output includes:
// - stateChanges: number of state transitions
// - totalFailures: number of times circuit opened
// - recoveryAttempts: number of recovery attempts
// - transitions: detailed history of all state changesComplete Example with Error Handling and State Monitoring
import {
CircuitBreaker,
ECircuitBreakerState,
CircuitOpenError,
CircuitBreakerError
} from '@apiratorjs/circuit-breaker';
// Define your risky operation
async function riskyOperation(data: any) {
// Simulate a service that fails sometimes
if (Math.random() < 0.7) {
throw new Error('Service temporarily unavailable');
}
return { success: true, data };
}
const circuitBreaker = new CircuitBreaker(riskyOperation, {
failureThreshold: 3,
durationOfBreakInMs: 30000,
successThreshold: 2
});
// Set up comprehensive state change monitoring
circuitBreaker.onStateChange((stateTransition, error) => {
console.log(`🔄 Circuit breaker: ${stateTransition.previousState} → ${stateTransition.newState}`);
if (error) {
console.log(`Triggered by error: ${error.message}`);
}
});
// Example usage with proper error handling
async function makeServiceCall(data: any) {
try {
const result = await circuitBreaker.execute(data);
console.log('✅ Service call successful:', result);
return result;
} catch (error) {
if (error instanceof CircuitOpenError) {
console.log('⚠️ Circuit is open - service temporarily unavailable');
console.log('Original cause:', error.cause?.message);
console.log(`⏱️ Next attempt in: ${error.durationTillNextAttemptInMs}ms`);
// Handle circuit open scenario (e.g., return cached data, show user message)
// You can use durationTillNextAttemptInMs for user feedback or retry scheduling
const retryTime = new Date(Date.now() + error.durationTillNextAttemptInMs);
console.log(`Service will retry at: ${retryTime.toLocaleTimeString()}`);
} else if (error instanceof CircuitBreakerError) {
console.log('🔧 Circuit breaker error:', error.toJSON());
} else {
console.log('❌ Service call failed:', error.message);
// Handle other service errors
}
throw error;
}
}
// Check current state
console.log('Current state:', circuitBreaker.state);
// Example of multiple calls to demonstrate state changes
async function demonstrateCircuitBreaker() {
for (let i = 0; i < 10; i++) {
try {
await makeServiceCall({ attempt: i + 1 });
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second
} catch (error) {
// Continue with next attempt
}
}
}Advanced Features
Fallback Function
Provide a fallback when the circuit is open:
const circuitBreaker = new CircuitBreaker(fetchData, {
failureThreshold: 3,
durationOfBreakInMs: 30000,
successThreshold: 2,
fallback: (userId) => {
// Return cached data instead of throwing error
return cache.get(userId) || { id: userId, name: 'Unknown' };
}
});
// When circuit is open, fallback is called automatically
const data = await circuitBreaker.execute('user-123');Error Filtering
Filter which errors should count as failures:
const circuitBreaker = new CircuitBreaker(apiCall, {
failureThreshold: 5,
durationOfBreakInMs: 60000,
successThreshold: 2,
errorFilter: (error) => {
// Ignore validation errors
if (error.name === 'ValidationError') return false;
// Only count 5xx server errors as failures
if ('status' in error) {
return error.status >= 500 && error.status < 600;
}
return true; // Count other errors
}
});Manual Circuit Control
Manually control circuit state for maintenance or testing:
const circuitBreaker = new CircuitBreaker(operation, options);
// Manual control
circuitBreaker.forceOpen(); // Open circuit (e.g., for maintenance)
circuitBreaker.forceClose(); // Close and reset
circuitBreaker.forceHalfOpen(); // Force to half-open state
// Check state
console.log(circuitBreaker.state); // 'open', 'closed', or 'half_open'Using decorator (for typescript projects)
Method Decorator (@WithCircuitBreaker)
Protect class methods using TypeScript decorators:
import { WithCircuitBreaker } from '@apiratorjs/circuit-breaker';
class UserService {
@WithCircuitBreaker({
failureThreshold: 5,
durationOfBreakInMs: 60000,
successThreshold: 2
})
async fetchUser(id: string) {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
@WithCircuitBreaker({
failureThreshold: 3,
durationOfBreakInMs: 45000,
successThreshold: 1
})
async updateUser(id: string, data: any) {
const response = await fetch(`https://api.example.com/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
}
// Usage
const userService = new UserService();
try {
const user = await userService.fetchUser('123');
await userService.updateUser('123', { name: 'John Doe' });
} catch (error) {
console.error('Service call failed:', error.message);
}Note: To use decorators, ensure your
tsconfig.jsonhas"experimentalDecorators": trueand"emitDecoratorMetadata": trueenabled.
Contributing
Contributions, issues, and feature requests are welcome! Feel free to check issues page.
License
This project is MIT licensed.