CircuitBreaker

import { CircuitBreaker } from "jsr:@ende/async/unstable-circuit-breaker";

class · experimental

class CircuitBreaker<T = unknown>

A circuit breaker that wraps async operations to prevent cascading failures.

The circuit breaker monitors the failure rate within a sliding window and "trips" (opens) when the rate exceeds a threshold, rejecting subsequent requests immediately without executing them. After a cooldown period, it enters a "half-open" state to test if the service has recovered.

The failure rate is only evaluated once at least minimumThroughput requests have been recorded in the window, preventing false positives during low traffic.

When state changes occur, callbacks fire in a fixed order:

  • Circuit trips open: onFailure → onStateChange → onOpen
  • Cooldown expires: onStateChange → onHalfOpen
  • Recovery completes: onStateChange → onClose

Example: Usage

import { CircuitBreaker } from "@ende/async/unstable-circuit-breaker";
import { assertEquals } from "@ende/assert";

const breaker = new CircuitBreaker({
  failureRateThreshold: 0.5,
  minimumThroughput: 20,
  cooldownMs: 30000,
});

assertEquals(breaker.state, "closed");

const result = await breaker.execute(() => Promise.resolve("success"));
assertEquals(result, "success");

Example: Handling open circuit

import { CircuitBreaker, CircuitBreakerOpenError } from "@ende/async/unstable-circuit-breaker";

const breaker = new CircuitBreaker();

try {
  const result = await breaker.execute(() => fetch("https://api.example.com"));
} catch (error) {
  if (error instanceof CircuitBreakerOpenError) {
    console.log(`Service unavailable, retry in ${error.remainingCooldownMs}ms`);
  }
}

Example: With custom failure detection

import { CircuitBreaker } from "@ende/async/unstable-circuit-breaker";

const breaker = new CircuitBreaker<Response>({
  failureRateThreshold: 0.1,
  isResultFailure: (response) => response.status >= 500,
  onStateChange: (from, to) => console.log(`Circuit: ${from} → ${to}`),
});

Example: Composing with retry and AbortSignal

import { retry } from "@ende/async/retry";
import { CircuitBreaker } from "@ende/async/unstable-circuit-breaker";

const breaker = new CircuitBreaker();

// Timeout applies to the entire operation (circuit breaker + retries)
const signal = AbortSignal.timeout(5000);

const result = await breaker.execute(
  () => retry(() => fetch("https://api.example.com"), { signal }),
  { signal },
);

Example: Waiting out the cooldown with delay

import { delay } from "@ende/async/delay";
import { CircuitBreaker, CircuitBreakerOpenError } from "@ende/async/unstable-circuit-breaker";

const breaker = new CircuitBreaker();

try {
  return await breaker.execute(() => fetch("https://api.example.com"));
} catch (error) {
  if (error instanceof CircuitBreakerOpenError) {
    // Wait for the circuit to transition to half-open, then retry
    await delay(error.remainingCooldownMs);
    return await breaker.execute(() => fetch("https://api.example.com"));
  }
  throw error;
}

Constructors

constructor

new CircuitBreaker(options: CircuitBreakerOptions<T> = [UNSUPPORTED])

experimental

Constructs a new CircuitBreaker instance.

options: CircuitBreakerOptions<T> = [UNSUPPORTED]

Configuration options for the circuit breaker.

Methods

state

get state(): CircuitState

experimental

Current state of the circuit breaker.

Time-based transitions (e.g. open to half-open after cooldown) are resolved lazily when this property is read.

Returns

The current CircuitState.

Example: Usage

import { CircuitBreaker } from "@ende/async/unstable-circuit-breaker";
import { assertEquals } from "@ende/assert";

const breaker = new CircuitBreaker();
assertEquals(breaker.state, "closed");

execute

execute<R extends T>(fn: () => R | Promise<R>, options?: CircuitBreakerExecuteOptions): Promise<R>

experimental

Executes a function through the circuit breaker.

The function can be synchronous or asynchronous. The result is always returned as a promise.

fn: () => R | Promise<R>

The function to execute through the circuit breaker.

options?: CircuitBreakerExecuteOptions

Options for this execution, such as an abort signal.

Returns

A promise that resolves to the result of the operation.

Example: Usage with async function

import { CircuitBreaker } from "@ende/async/unstable-circuit-breaker";

const breaker = new CircuitBreaker();
const response = await breaker.execute(() => fetch("https://api.example.com"));

Example: With timeout

import { CircuitBreaker } from "@ende/async/unstable-circuit-breaker";

const breaker = new CircuitBreaker();

// Abort if operation takes longer than 5 seconds
const response = await breaker.execute(
  () => fetch("https://api.example.com"),
  { signal: AbortSignal.timeout(5000) },
);

forceOpen

forceOpen(): void

experimental

Forces the circuit breaker to open state. Useful for maintenance or known outages.

Returns

void

Example: Usage

import { CircuitBreaker } from "@ende/async/unstable-circuit-breaker";
import { assertEquals } from "@ende/assert";

const breaker = new CircuitBreaker();
breaker.forceOpen();
assertEquals(breaker.state, "open");

forceClose

forceClose(): void

experimental

Forces the circuit breaker to closed state and notifies observers.

This is an operational transition that fires onStateChange and onClose callbacks. Use this when the protected service has recovered and you want observers to be notified.

For silent resets (e.g., in tests), use reset instead.

Returns

void

Example: Usage

import { CircuitBreaker } from "@ende/async/unstable-circuit-breaker";
import { assertEquals } from "@ende/assert";

const breaker = new CircuitBreaker();
breaker.forceOpen();
breaker.forceClose();
assertEquals(breaker.state, "closed");

reset

reset(): void

experimental

Silently resets the circuit breaker to initial state.

Unlike forceClose, this does not fire any callbacks (onStateChange, onClose). Use this for testing or administrative resets where observers should not be notified.

Returns

void

Example: Usage

import { CircuitBreaker } from "@ende/async/unstable-circuit-breaker";
import { assertEquals } from "@ende/assert";

const breaker = new CircuitBreaker();
breaker.reset();
assertEquals(breaker.state, "closed");

Source