Skip to content
Buy me a coffee

Chaos

Chaos strategies make a healthy dependency look broken, on purpose and at a controlled rate. Use them to check that the rest of the pipeline does its job: that the retry really retries, the timeout fires, the breaker opens and the fallback answers. Run them in tests, staging, and carefully in production. They are a port of Polly’s Simmy, with the same option and event names. Don’t use them as a test double for business logic; a mock is simpler.

import {
class ResiliencePipelineBuilder<T = any, E = never, C extends string = never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
} from '@ylcnfrht/resilience';
const
const pipeline: ResiliencePipeline<any, TimeoutRejectedError>
pipeline
= new
new ResiliencePipelineBuilder<any, never, never>(options?: ResiliencePipelineBuilderOptions): ResiliencePipelineBuilder<any, never, never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
()
.
ResiliencePipelineBuilder<any, never, never>.addRetry(options?: RetryOptions<any>): ResiliencePipelineBuilder<any, never, never>
addRetry
({
RetryOptions<any>.maxRetryAttempts?: number | undefined

Retries in addition to the original call. Default 3. Infinity retries until aborted.

maxRetryAttempts
: 3 })
.
ResiliencePipelineBuilder<any, never, never>.addTimeout(options: Duration | TimeoutOptions): Next<any, never, never, TimeoutRejectedError>

addTimeout(5000), addTimeout('5s') or addTimeout({ timeout: '5s', onTimeout }).

addTimeout
('1s')
.
ResiliencePipelineBuilder<any, TimeoutRejectedError, never>.addChaosLatency(options: ChaosLatencyOptions): Next<any, never, never, TimeoutRejectedError>
addChaosLatency
({
ChaosOptions.injectionRate?: number | undefined

Probability in [0, 1]. Default 0.001. Ignored when injectionRateGenerator is set.

injectionRate
: 0.1,
ChaosLatencyOptions.latency?: Duration | undefined

Default 30 s.

latency
: '2s' }) // 10%: slower than the timeout
.
ResiliencePipelineBuilder<any, TimeoutRejectedError, never>.addChaosFault(options: ChaosFaultOptions): Next<any, never, never, TimeoutRejectedError>
addChaosFault
({
ChaosOptions.injectionRate?: number | undefined

Probability in [0, 1]. Default 0.001. Ignored when injectionRateGenerator is set.

injectionRate
: 0.05,
ChaosFaultOptions.fault?: unknown
fault
: new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('chaos: connection reset') })
.
ResiliencePipelineBuilder<any, TimeoutRejectedError, never>.build(): ResiliencePipeline<any, TimeoutRejectedError>
build
();

Add chaos strategies last (innermost), right before the callback, so every strategy you want to test sees the injected failure as if it came from the dependency.

Builder method Definition Injects Then
addChaosFault chaosFaultStrategy An error (fault / faultGenerator) Fails; the callback doesn’t run.
addChaosOutcome chaosOutcomeStrategy A result or error (outcome / outcomeGenerator) Returns it; the callback doesn’t run.
addChaosLatency chaosLatencyStrategy A delay (latency / latencyGenerator) Runs the callback after the delay.
addChaosBehavior chaosBehaviorStrategy Any side effect (behavior) Runs the callback after it.

Each execution: if enabled, draw randomizer(); inject when it’s below the injection rate. A generator that returns undefined (fault or outcome) skips injection for that call. An abort during injected latency fails the execution with the abort reason.

import {
type Outcome<T, E = unknown> = {
readonly ok: true;
readonly result: T;
} | {
readonly ok: false;
readonly error: E;
}
const Outcome: {
fromResult: <T>(result: T) => Outcome<T, never>;
fromError: <T = never, E = unknown>(error: E) => Outcome<T, E>;
is: (value: unknown) => value is Outcome<unknown, unknown>;
from: (value: unknown) => Outcome<unknown, unknown> | undefined;
try: {
<R>(fn: () => R): TryResult<R, unknown>;
<R, E>(options: {
try: () => R;
catch: (error: unknown) => E;
}): TryResult<R, E>;
};
all<const O extends readonly Outcome<any, any>[]>(outcomes: O): Outcome<{ -readonly [K in keyof O]: OutcomeValue<O[K]>; }, OutcomeError<O[number]>>;
partition<O extends Outcome<any, any>>(outcomes: readonly O[]): {
values: OutcomeValue<O>[];
errors: OutcomeError<O>[];
};
unwrap: <T>(outcome: Outcome<T, unknown>) => T;
unwrapErr<E, C extends ErrorCodes<E> = ErrorCodes<...>>(outcome: Outcome<unknown, E>, code?: C | undefined): ErrorWithCode<E, C>;
match<T, E, H extends OutcomeHandlers<T, E, unknown>>(outcome: Outcome<T, E>, handlers: H): ReturnType<H[keyof H] & ((...args: any) => any)>;
}

The result of an execution: either a value or an error. Strategies pass outcomes around instead of throwing. E is the error type when it's known (see pipeline.settle()), unknown otherwise.

Outcome
,
class ResiliencePipelineBuilder<T = any, E = never, C extends string = never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
,
class TimeoutRejectedError
TimeoutRejectedError
,
function weighted<V>(entries: ReadonlyArray<{
readonly weight: number;
readonly value: V | (() => V);
}>, randomizer?: () => number): () => V

Picks one of several faults/outcomes by weight (Polly's FaultGenerator / OutcomeGenerator): faultGenerator: weighted([{ weight: 80, value: () => new TimeoutError() }, { weight: 20, value: () => new Error('500') }]). Values may be factories (called per injection) or constants.

weighted
} from '@ylcnfrht/resilience';
const
const pipeline: ResiliencePipeline<Response, never>
pipeline
= new
new ResiliencePipelineBuilder<Response, never, never>(options?: ResiliencePipelineBuilderOptions): ResiliencePipelineBuilder<Response, never, never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
<
interface Response

The Response interface of the Fetch API represents the response to a request.

MDN Reference

Response
>()
// A fake 503 answer, as if the API were overloaded.
.
ResiliencePipelineBuilder<Response, never, never>.addChaosOutcome(options: ChaosOutcomeOptions<Response>): ResiliencePipelineBuilder<Response, never, never>
addChaosOutcome
({
ChaosOptions.injectionRate?: number | undefined

Probability in [0, 1]. Default 0.001. Ignored when injectionRateGenerator is set.

injectionRate
: 0.02,
ChaosOutcomeOptions<Response>.outcome?: Outcome<Response> | undefined
outcome
:
const Outcome: {
fromResult: <T>(result: T) => Outcome<T, never>;
fromError: <T = never, E = unknown>(error: E) => Outcome<T, E>;
is: (value: unknown) => value is Outcome<unknown, unknown>;
from: (value: unknown) => Outcome<unknown, unknown> | undefined;
try: {
<R>(fn: () => R): TryResult<R, unknown>;
<R, E>(options: {
try: () => R;
catch: (error: unknown) => E;
}): TryResult<R, E>;
};
all<const O extends readonly Outcome<any, any>[]>(outcomes: O): Outcome<{ -readonly [K in keyof O]: OutcomeValue<O[K]>; }, OutcomeError<O[number]>>;
partition<O extends Outcome<any, any>>(outcomes: readonly O[]): {
values: OutcomeValue<O>[];
errors: OutcomeError<O>[];
};
unwrap: <T>(outcome: Outcome<T, unknown>) => T;
unwrapErr<E, C extends ErrorCodes<E> = ErrorCodes<...>>(outcome: Outcome<unknown, E>, code?: C | undefined): ErrorWithCode<E, C>;
match<T, E, H extends OutcomeHandlers<T, E, unknown>>(outcome: Outcome<T, E>, handlers: H): ReturnType<H[keyof H] & ((...args: any) => any)>;
}

The result of an execution: either a value or an error. Strategies pass outcomes around instead of throwing. E is the error type when it's known (see pipeline.settle()), unknown otherwise.

Outcome
.
fromResult: <Response>(result: Response) => Outcome<Response, never>
fromResult
(new
var Response: new (body?: BodyInit | null, init?: ResponseInit) => Response

The Response interface of the Fetch API represents the response to a request.

MDN Reference

Response
(null, {
ResponseInit.status?: number | undefined
status
: 503 })) })
// 80% timeouts, 20% resets, picked per injection.
.
ResiliencePipelineBuilder<Response, never, never>.addChaosFault(options: ChaosFaultOptions): ResiliencePipelineBuilder<Response, never, never>
addChaosFault
({
ChaosOptions.injectionRate?: number | undefined

Probability in [0, 1]. Default 0.001. Ignored when injectionRateGenerator is set.

injectionRate
: 0.05,
ChaosFaultOptions.faultGenerator?: ((args: ContextArgs) => MaybePromise<unknown>) | undefined
faultGenerator
:
weighted<Error>(entries: readonly {
readonly weight: number;
readonly value: Error | (() => Error);
}[], randomizer?: () => number): () => Error

Picks one of several faults/outcomes by weight (Polly's FaultGenerator / OutcomeGenerator): faultGenerator: weighted([{ weight: 80, value: () => new TimeoutError() }, { weight: 20, value: () => new Error('500') }]). Values may be factories (called per injection) or constants.

weighted
([
{
weight: number
weight
: 80,
value: Error | (() => Error)
value
: () => new
new TimeoutRejectedError(timeout: number): TimeoutRejectedError
TimeoutRejectedError
(1000) },
{
weight: number
weight
: 20,
value: Error | (() => Error)
value
: () => new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('ECONNRESET') },
]),
})
// Random latency between 0 and 3 s.
.
ResiliencePipelineBuilder<Response, never, never>.addChaosLatency(options: ChaosLatencyOptions): ResiliencePipelineBuilder<Response, never, never>
addChaosLatency
({
ChaosOptions.injectionRate?: number | undefined

Probability in [0, 1]. Default 0.001. Ignored when injectionRateGenerator is set.

injectionRate
: 0.1,
ChaosLatencyOptions.latencyGenerator?: ((args: ContextArgs) => MaybePromise<Duration>) | undefined
latencyGenerator
: () =>
var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.
Math.random(): number

Returns a pseudorandom number between 0 and 1.

random
() * 3000 })
.
ResiliencePipelineBuilder<Response, never, never>.build(): ResiliencePipeline<Response, never>
build
();

weighted(entries, randomizer?) returns a function that picks one entry by weight (Polly’s FaultGenerator / OutcomeGenerator). Values may be constants or factories called per pick. Weights must be ≥ 0 and sum to more than 0.

Option Type Default Description
injectionRate number 0.001 Probability per execution, in [0, 1]. Ignored when injectionRateGenerator is set.
injectionRateGenerator (args: { context }) => number | Promise<number> – Rate per execution, e.g. from a feature flag or a header.
enabled boolean true Master switch. Ignored when enabledGenerator is set.
enabledGenerator (args: { context }) => boolean | Promise<boolean> – Switch per execution. Checked before the rate.
randomizer () => number Math.random Source of randomness in [0, 1). Seed it for reproducible tests.
name string 'Chaos.Fault', 'Chaos.Outcome', 'Chaos.Latency' or 'Chaos.Behavior' Strategy name.
Strategy Option Type Default Description
Fault fault unknown – The error to fail with, used as is (the same instance every time; a function is not called). fault or faultGenerator is required.
Fault faultGenerator (args: { context }) => unknown – Error per injection; undefined skips. Wins over fault.
Fault onFaultInjected (args: { context, fault }) => void | Promise<void> – After a fault is chosen.
Outcome outcome Outcome<T> – Outcome.fromResult(value) or Outcome.fromError(error). outcome or outcomeGenerator is required.
Outcome outcomeGenerator (args: { context }) => Outcome<T> | undefined – Outcome per injection; undefined skips. Wins over outcome.
Outcome onOutcomeInjected (args: { context, outcome }) => void | Promise<void> – After an outcome is chosen.
Latency latency Duration 30_000 (30 s) Delay before the callback.
Latency latencyGenerator (args: { context }) => Duration – Delay per injection. Wins over latency.
Latency onLatencyInjected (args: { context, latency }) => void | Promise<void> – Before the delay; latency in ms.
Behavior behavior (args: { context }) => void | Promise<void> required The side effect, e.g. kill a connection or fill a disk in a test.
Behavior onBehaviorInjected (args: { context }) => void | Promise<void> – Before behavior runs.

Generators may return promises.

Event names match Polly, so dashboards built for Polly’s telemetry keep working.

Telemetry event Callback Args
Chaos.OnFault onFaultInjected { context, fault }
Chaos.OnOutcome onOutcomeInjected { context, outcome }
Chaos.OnLatency onLatencyInjected { context, latency }
Chaos.OnBehavior onBehaviorInjected { context }
Chaos.OnGlobal – { context, latency, fault } (setGlobalChaos; fault is a boolean)

The strategies raise no errors of their own: they raise whatever you inject.

setGlobalChaos(options) injects faults or latency into every pipeline in the process, without touching their configuration. It runs before each callback attempt, so retries and hedges are hit too. Use it for game days and soak tests: “does the whole app survive a slow database?”. It returns a function that turns it off.

import {
function setGlobalChaos(options: GlobalChaosOptions): () => void

Inject faults and/or latency into every pipeline in the process, without touching their configuration: game days, staging soak tests, "does my whole app survive a slow database?". Returns a function that turns it off.

setGlobalChaos
} from '@ylcnfrht/resilience';
const
const stop: () => void
stop
=
function setGlobalChaos(options: GlobalChaosOptions): () => void

Inject faults and/or latency into every pipeline in the process, without touching their configuration: game days, staging soak tests, "does my whole app survive a slow database?". Returns a function that turns it off.

setGlobalChaos
({
GlobalChaosOptions.injectionRate?: number | undefined

Probability per callback in [0, 1]. Default 0.05.

injectionRate
: 0.1,
GlobalChaosOptions.latency?: Duration | undefined

Delay added before the callback.

latency
: '2s',
GlobalChaosOptions.filter?: ((context: ResilienceContext) => boolean) | undefined

Only inject where this returns true (e.g. one pipeline, or one operationKey).

filter
: (
ctx: ResilienceContext
ctx
) =>
ctx: ResilienceContext
ctx
.
ResilienceContext.operationKey: string | undefined
operationKey
=== 'db',
});
// ... every matching call is now 2 s slower 10% of the time
const stop: () => void
stop
();
Option Type Default Description
injectionRate number 0.05 Probability per callback attempt, in [0, 1].
filter (context) => boolean every call Inject only where it returns true (one operationKey, name or property; the context carries no pipeline name).
latency Duration – Delay before the callback.
fault unknown | (context) => unknown – Error to fail with (or a function creating it), after the latency. Omit to inject latency only. fault or latency is required.
randomizer () => number Math.random Source of randomness.

Only one global chaos configuration is active at a time: a second call replaces the first, and the first one’s stop() then does nothing.

const
const pipeline: ResiliencePipeline<any, never>
pipeline
= new
new ResiliencePipelineBuilder<any, never, never>(options?: ResiliencePipelineBuilderOptions): ResiliencePipelineBuilder<any, never, never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
()
.
ResiliencePipelineBuilder<any, never, never>.addRetry(options?: RetryOptions<any>): ResiliencePipelineBuilder<any, never, never>
addRetry
()
.
ResiliencePipelineBuilder<any, never, never>.addChaosFault(options: ChaosFaultOptions): ResiliencePipelineBuilder<any, never, never>
addChaosFault
({
ChaosOptions.enabledGenerator?: ((args: ContextArgs) => MaybePromise<boolean>) | undefined
enabledGenerator
: () =>
const flags: {
isOn(name: string): boolean;
number(name: string): number;
}
flags
.
function isOn(name: string): boolean
isOn
('chaos.payments'),
// Only synthetic traffic, never real customers.
ChaosOptions.injectionRateGenerator?: ((args: ContextArgs) => MaybePromise<number>) | undefined
injectionRateGenerator
: ({
context: ResilienceContext
context
}) => (
context: ResilienceContext
context
.
ResilienceContext.properties: Map<string, unknown>

Shared bag for passing data between strategies and the callback.

properties
.
Map<string, unknown>.get(key: string): unknown

Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.

@returns ― Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.

get
('synthetic') ?
const flags: {
isOn(name: string): boolean;
number(name: string): number;
}
flags
.
function number(name: string): number
number
('chaos.payments.rate') : 0),
ChaosFaultOptions.faultGenerator?: ((args: ContextArgs) => MaybePromise<unknown>) | undefined
faultGenerator
: () => new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('chaos: payments down'),
})
.
ResiliencePipelineBuilder<any, never, never>.build(): ResiliencePipeline<any, never>
build
();
  • Inject only into calls that are safe to fail: synthetic traffic, read paths, internal tenants.
  • Watch the Chaos.* events next to your error rates, so an injected failure is never mistaken for a real one.
  • Seed randomizer in tests: injectionRate: 1 or a fixed sequence makes runs reproducible.