@WithCircuitBreaker Decorator
@WithCircuitBreaker wraps a method so every call goes through the breaker for a given key. Unlike a NestJS guard, it is proxy-based and works on any Injectable method — services, repositories, gateways — not just controllers (the same pattern as @WithLock and @Cached).
Usage
import { Injectable } from '@nestjs/common';
import { WithCircuitBreaker } from '@nestjs-redisx/circuit-breaker';
import { PaymentGateway, UsersApi, UserCache, Charge, User } from './types';
@Injectable()
export class PaymentsService {
constructor(
private readonly gateway: PaymentGateway,
private readonly usersApi: UsersApi,
private readonly userCache: UserCache,
) {}
// Trips after repeated failures; throws CircuitBreakerOpenError while OPEN.
@WithCircuitBreaker({ key: 'stripe', failureThreshold: 5, openDurationMs: 30000 })
async charge(charge: Charge): Promise<{ ok: boolean }> {
return this.gateway.charge(charge);
}
// Key interpolated from the first argument; falls back to cache when OPEN.
@WithCircuitBreaker({
key: 'users-api:{0}',
fallback: (id: string) => ({ id, name: 'cached' }),
})
async getUser(id: string): Promise<User> {
return this.usersApi.getUser(id);
}
// Skip execution (resolve to undefined) instead of throwing while OPEN.
@WithCircuitBreaker({ key: 'users-api', onOpen: 'skip' })
async warmCache(id: string): Promise<User | null> {
return this.userCache.get(id);
}
// Bypass the breaker entirely for trusted/internal calls via skip().
@WithCircuitBreaker({
key: 'users-api',
skip: (id: string, internal?: boolean) => internal === true,
})
async getUserMaybeInternal(id: string, internal?: boolean): Promise<User> {
return this.usersApi.getUser(id);
}
}Options
IWithCircuitBreakerOptions:
| Option | Type | Description |
|---|---|---|
key | string | (...args) => string | Circuit key. Strings support {0}, {1.id} interpolation from arguments. |
failureThreshold | number | Per-method override. |
windowMs | number | Per-method override. |
openDurationMs | number | Per-method override. |
halfOpenMaxCalls | number | Per-method override. |
successThreshold | number | Per-method override. |
probeTimeoutMs | number | Per-method override (defaults to the resolved openDurationMs). |
fallback | (...args) => unknown | Called with the original arguments when the breaker rejects; its return becomes the method result. |
onOpen | 'throw' | 'skip' | When there is no fallback: throw CircuitBreakerOpenError (default) or skip and resolve to undefined. |
skip | (...args) => boolean | Promise<boolean> | Evaluated with the method arguments; when it returns true the method runs directly, bypassing the breaker (no state read or recorded). |
Behaviour
- CLOSED — the method runs normally; failures (thrown errors) are counted.
- OPEN — the method is not executed. With a
fallback, its value is returned; withonOpen: 'skip',undefinedis returned; otherwiseCircuitBreakerOpenErroris thrown. - HALF_OPEN — a limited number of calls are allowed through as probes.
TIP
The decorator throwing behaviour comes from the breaker rejecting the call. Recording of success/failure is automatic — a resolved method records success, a thrown error records failure.
WARNING
If the plugin has not finished initializing (no service available yet), the method runs without the breaker and a warning is logged — calls are never blocked by a missing breaker.
Known typing limitation
fallback and skip are typed as (...args: unknown[]) so a single decorator works for any method signature (the same trade-off as @WithLock's key builder). Under strict TypeScript, annotate the callback parameters as unknown and narrow inside, or cast: skip: ((id: string) => boolean) as (...args: unknown[]) => boolean.