Service API
Inject CIRCUIT_BREAKER_SERVICE for full programmatic control when a decorator is not a good fit.
import { Injectable, Inject } from '@nestjs/common';
import { CIRCUIT_BREAKER_SERVICE, ICircuitBreakerService } from '@nestjs-redisx/circuit-breaker';
import { UsersApi, UserCache, User } from './types';
@Injectable()
export class UsersService {
constructor(
@Inject(CIRCUIT_BREAKER_SERVICE)
private readonly breaker: ICircuitBreakerService,
private readonly usersApi: UsersApi,
private readonly userCache: UserCache,
) {}
async getUser(id: string): Promise<User> {
// Guard the call; serve a cached value if the breaker is OPEN.
return this.breaker.execute(`users-api:${id}`, () => this.usersApi.getUser(id), {
fallback: async () => {
const cached = await this.userCache.get(id);
return cached ?? { id, name: 'unknown' };
},
});
}
async status(id: string): Promise<string> {
const snapshot = await this.breaker.getState(`users-api:${id}`);
return snapshot.state; // 'closed' | 'open' | 'half-open'
}
async reportOutcome(id: string, ok: boolean): Promise<void> {
// Manual recording (e.g. from a health probe) without execute().
if (ok) {
await this.breaker.recordSuccess(`users-api:${id}`);
} else {
await this.breaker.recordFailure(`users-api:${id}`);
}
}
async clear(id: string): Promise<void> {
await this.breaker.reset(`users-api:${id}`);
}
}ICircuitBreakerService
execute<T>(key, fn, options?): Promise<T>— runfnguarded by the breaker. On success records success; on throw records failure and rethrows. When the breaker rejects the call, returnsoptions.fallback()if provided, otherwise throws theerrorFactoryerror orCircuitBreakerOpenError.recordSuccess(key, options?): Promise<ICircuitSnapshot>— manually record a success (e.g. from an external health probe).recordFailure(key, options?): Promise<ICircuitSnapshot>— manually record a failure.getState(key, options?): Promise<ICircuitSnapshot>— read the committed state without mutating it (does not flip OPEN → HALF_OPEN).reset(key): Promise<void>— return the circuit to CLOSED and clear all state.
ICircuitSnapshot
interface ICircuitSnapshot {
state: 'closed' | 'open' | 'half-open';
failuresInWindow: number; // CLOSED: failures still inside the window
halfOpenSuccesses: number; // HALF_OPEN: probes that have succeeded
halfOpenInFlight: number; // HALF_OPEN: unresolved probes still within probeTimeoutMs
}Execute options
ICircuitBreakerExecuteOptions extends the per-call overrides (failureThreshold, windowMs, openDurationMs, halfOpenMaxCalls, successThreshold, probeTimeoutMs) with:
fallback?: () => T | Promise<T>— returned instead of throwing when rejected.errorFactory?: (key, snapshot) => Error— custom rejection error (overrides the plugin-level factory).
Store errors vs open rejections
A rejection because the breaker is OPEN always surfaces as a fallback/CircuitBreakerOpenError. A failure of the state store (Redis) on the execute() gate is governed by errorPolicy — fail-open runs fn anyway, fail-closed throws CircuitBreakerStoreError. Recording success/failure never masks your function's own result or error.
The manual API is always strict
recordSuccess, recordFailure, getState, and reset are not subject to errorPolicy: when the state store fails they always throw CircuitBreakerStoreError. There is no meaningful "fail-open" result for an explicit state operation — silently dropping a manual recordFailure would corrupt operator expectations.
Invalid configuration always throws
Plugin options and per-call overrides are validated (integers ≥ 1, successThreshold <= halfOpenMaxCalls). An invalid config throws InvalidCircuitBreakerConfigError immediately — it is a programmer error and is never subject to errorPolicy.