Skip to content

Testing resilience code

The problem. A pipeline with delay: '2s' retries and a breakDuration: '30s' breaker is slow to test with a real clock. Tests either wait (slow), or shrink every duration to a few milliseconds (flaky on a busy CI machine, and no longer the configuration you ship).

The solution. Every strategy reads time through a TimeProvider. In tests, pass a FakeTimeProvider from resilience-pipeline/testing and move the clock by hand. A test of “three retries 10 s apart, then a 1 s timeout” finishes in a few milliseconds and does exactly the same thing every run.

import {
function test(name?: string, fn?: test.TestFn): Promise<void> (+3 overloads)

The test() function is the value imported from the test module. Each invocation of this function results in reporting the test to the TestsStream.

The TestContext object passed to the fn argument can be used to perform actions related to the current test. Examples include skipping the test, adding additional diagnostic information, or creating subtests.

test() returns a Promise that fulfills once the test completes. if test() is called within a suite, it fulfills immediately. The return value can usually be discarded for top level tests. However, the return value from subtests should be used to prevent the parent test from finishing first and cancelling the subtest as shown in the following example.

test('top level test', async (t) => {
// The setTimeout() in the following subtest would cause it to outlive its
// parent test if 'await' is removed on the next line. Once the parent test
// completes, it will cancel any outstanding subtests.
await t.test('longer running subtest', async (t) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
});
});

The timeout option can be used to fail the test if it takes longer than timeout milliseconds to complete. However, it is not a reliable mechanism for canceling tests because a running test might block the application thread and thus prevent the scheduled cancellation.

@since ― v18.0.0, v16.17.0

@param ― name The name of the test, which is displayed when reporting test results. Defaults to the name property of fn, or '<anonymous>' if fn does not have a name.

@param ― options Configuration options for the test.

@param ― fn The function under test. The first argument to this function is a TestContext object. If the test uses callbacks, the callback function is passed as the second argument.

@return ― Fulfilled with undefined once the test completes, or immediately if the test runs within a suite.

test
} from 'node:test';
import
function assert(value: unknown, message?: string | Error): asserts value
assert
from 'node:assert/strict';
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
,
class TimeoutRejectedError
TimeoutRejectedError
} from 'resilience-pipeline';
import {
class FakeTimeProvider

A clock you move by hand, so retries, timeouts and open circuits can be tested without waiting.

const time = new FakeTimeProvider();
const pipeline = new ResiliencePipelineBuilder({ timeProvider: time }).addRetry({ delay: '10s' }).addTimeout('1s').build();
// Attach the expectation first: the promise settles while runAll() fires timers.
const done = assert.rejects(pipeline.execute(() => neverResolves()), TimeoutRejectedError);
await time.runAll(); // 4 timeouts and 3 delays, instantly
await done;

FakeTimeProvider
} from 'resilience-pipeline/testing';
function test(name?: string, fn?: test.TestFn): Promise<void> (+3 overloads)

The test() function is the value imported from the test module. Each invocation of this function results in reporting the test to the TestsStream.

The TestContext object passed to the fn argument can be used to perform actions related to the current test. Examples include skipping the test, adding additional diagnostic information, or creating subtests.

test() returns a Promise that fulfills once the test completes. if test() is called within a suite, it fulfills immediately. The return value can usually be discarded for top level tests. However, the return value from subtests should be used to prevent the parent test from finishing first and cancelling the subtest as shown in the following example.

test('top level test', async (t) => {
// The setTimeout() in the following subtest would cause it to outlive its
// parent test if 'await' is removed on the next line. Once the parent test
// completes, it will cancel any outstanding subtests.
await t.test('longer running subtest', async (t) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
});
});

The timeout option can be used to fail the test if it takes longer than timeout milliseconds to complete. However, it is not a reliable mechanism for canceling tests because a running test might block the application thread and thus prevent the scheduled cancellation.

@since ― v18.0.0, v16.17.0

@param ― name The name of the test, which is displayed when reporting test results. Defaults to the name property of fn, or '<anonymous>' if fn does not have a name.

@param ― options Configuration options for the test.

@param ― fn The function under test. The first argument to this function is a TestContext object. If the test uses callbacks, the callback function is passed as the second argument.

@return ― Fulfilled with undefined once the test completes, or immediately if the test runs within a suite.

test
('gives up after 3 retries and 4 timeouts', async () => {
const
const time: FakeTimeProvider
time
= new
new FakeTimeProvider(start?: number | Date): FakeTimeProvider

A clock you move by hand, so retries, timeouts and open circuits can be tested without waiting.

const time = new FakeTimeProvider();
const pipeline = new ResiliencePipelineBuilder({ timeProvider: time }).addRetry({ delay: '10s' }).addTimeout('1s').build();
// Attach the expectation first: the promise settles while runAll() fires timers.
const done = assert.rejects(pipeline.execute(() => neverResolves()), TimeoutRejectedError);
await time.runAll(); // 4 timeouts and 3 delays, instantly
await done;

FakeTimeProvider
();
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
({
ResiliencePipelineBuilderOptions.timeProvider?: TimeProvider | undefined

Clock and timers for every strategy. Default: the system clock. Use FakeTimeProvider in tests.

timeProvider
:
const time: FakeTimeProvider
time
})
.
ResiliencePipelineBuilder<any, never, never>.addRetry(options?: RetryOptions<any>): ResiliencePipelineBuilder<any, never, never>
addRetry
({
RetryOptions<any>.delay?: Duration | undefined

Base delay. Default 2000 ms.

delay
: '10s',
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>.build(): ResiliencePipeline<any, TimeoutRejectedError>
build
();
const
const start: number
start
=
const time: FakeTimeProvider
time
.
FakeTimeProvider.now(): number

Epoch milliseconds.

now
();
// 1. Start the call and attach the expectation. Don't await yet.
const
const done: Promise<void>
done
=
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.rejects(block: (() => Promise<unknown>) | Promise<unknown>, error: assert.AssertPredicate, message?: string | Error): Promise<void> (+1 overload)
export strict.rejects

Awaits the asyncFn promise or, if asyncFn is a function, immediately calls the function and awaits the returned promise to complete. It will then check that the promise is rejected.

If asyncFn is a function and it throws an error synchronously, assert.rejects() will return a rejected Promise with that error. If the function does not return a promise, assert.rejects() will return a rejected Promise with an ERR_INVALID_RETURN_VALUE error. In both cases the error handler is skipped.

Besides the async nature to await the completion behaves identically to

throws

.

If specified, error can be a Class, RegExp, a validation function, an object where each property will be tested for, or an instance of error where each property will be tested for including the non-enumerable message and name properties.

If specified, message will be the message provided by the `

AssertionError

if theasyncFn` fails to reject.

import assert from 'node:assert/strict';
await assert.rejects(
async () => {
throw new TypeError('Wrong value');
},
{
name: 'TypeError',
message: 'Wrong value',
},
);
import assert from 'node:assert/strict';
await assert.rejects(
async () => {
throw new TypeError('Wrong value');
},
(err) => {
assert.strictEqual(err.name, 'TypeError');
assert.strictEqual(err.message, 'Wrong value');
return true;
},
);
import assert from 'node:assert/strict';
assert.rejects(
Promise.reject(new Error('Wrong value')),
Error,
).then(() => {
// ...
});

error cannot be a string. If a string is provided as the second argument, then error is assumed to be omitted and the string will be used for message instead. This can lead to easy-to-miss mistakes. Please read the example in

throws

carefully if using a string as the second argument gets considered.

@since ― v10.0.0

rejects
(
const pipeline: ResiliencePipeline<any, TimeoutRejectedError>
pipeline
.
ResiliencePipeline<any, TimeoutRejectedError>.execute<unknown>(callback: (context: ResilienceContext) => unknown, options?: ExecuteOptions): Promise<unknown>

Runs the callback through the pipeline; throws the final error.

execute
(() => new
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>

Creates a new Promise.

@param ― executor A callback used to initialize the promise. This callback is passed two arguments: a resolve callback used to resolve the promise with a value or the result of another promise, and a reject callback used to reject the promise with a provided reason or error.

Promise
(() => {})),
class TimeoutRejectedError
TimeoutRejectedError
);
// 2. Fire every timer: 4 timeouts of 1 s + 3 delays of 10 s.
await
const time: FakeTimeProvider
time
.
FakeTimeProvider.runAll(limit?: number): Promise<void>

Fires timers until none are left (including ones scheduled while firing). Throws after limit timers to catch infinite loops.

runAll
();
// 3. Now the promise has settled.
await
const done: Promise<void>
done
;
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.equal<34000>(actual: unknown, expected: 34000, message?: string | Error): asserts actual is 34000
export strict.equal

Tests strict equality between the actual and expected parameters as determined by Object.is().

import assert from 'node:assert/strict';
assert.strictEqual(1, 2);
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
//
// 1 !== 2
assert.strictEqual(1, 1);
// OK
assert.strictEqual('Hello foobar', 'Hello World!');
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
// + actual - expected
//
// + 'Hello foobar'
// - 'Hello World!'
// ^
const apples = 1;
const oranges = 2;
assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
// TypeError: Inputs are not identical

If the values are not strictly equal, an AssertionError is thrown with a message property set equal to the value of the message parameter. If the message parameter is undefined, a default error message is assigned. If the message parameter is an instance of an Error then it will be thrown instead of the AssertionError.

@since ― v0.1.21

equal
(
const time: FakeTimeProvider
time
.
FakeTimeProvider.now(): number

Epoch milliseconds.

now
() -
const start: number
start
, 34_000);
});

FakeTimeProvider has three controls:

Member What it does
advance(duration) Moves the clock forward by duration ('30s', 500…), firing every timer that comes due, in order.
runAll(limit = 10_000) Fires timers until none are left, including timers scheduled while firing. Throws after limit timers.
pendingTimers How many timers are scheduled and not fired yet. 0 at the end of a test means nothing is left running.

The clock starts at 2026-01-01T00:00:00Z. Pass new FakeTimeProvider(0) or a Date to start elsewhere. Between two timers, the fake clock yields to the event loop with a real setTimeout(0), so every promise continuation (your callback’s awaits, the strategies’ own) runs before the next timer fires.

Attach the assertion before you move the clock

Section titled “Attach the assertion before you move the clock”

pipeline.execute() can’t settle until its timers fire. Two ways to get this wrong:

  • await pipeline.execute(...) first, then time.runAll(): the test hangs. The await never returns, because nothing moves the clock.
  • Start the call without a handler, then await time.runAll(): the call rejects during runAll(), while nothing is listening. Node reports an unhandled rejection and fails the test run.

So the order is always: start the call, attach the handler (assert.rejects(promise), promise.catch(...), or just keep the promise for a later await if it resolves), then move the clock, then await.

advance() lets you assert on the state between timers, for example that a retry does not start one millisecond early:

import {
function test(name?: string, fn?: test.TestFn): Promise<void> (+3 overloads)

The test() function is the value imported from the test module. Each invocation of this function results in reporting the test to the TestsStream.

The TestContext object passed to the fn argument can be used to perform actions related to the current test. Examples include skipping the test, adding additional diagnostic information, or creating subtests.

test() returns a Promise that fulfills once the test completes. if test() is called within a suite, it fulfills immediately. The return value can usually be discarded for top level tests. However, the return value from subtests should be used to prevent the parent test from finishing first and cancelling the subtest as shown in the following example.

test('top level test', async (t) => {
// The setTimeout() in the following subtest would cause it to outlive its
// parent test if 'await' is removed on the next line. Once the parent test
// completes, it will cancel any outstanding subtests.
await t.test('longer running subtest', async (t) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
});
});

The timeout option can be used to fail the test if it takes longer than timeout milliseconds to complete. However, it is not a reliable mechanism for canceling tests because a running test might block the application thread and thus prevent the scheduled cancellation.

@since ― v18.0.0, v16.17.0

@param ― name The name of the test, which is displayed when reporting test results. Defaults to the name property of fn, or '<anonymous>' if fn does not have a name.

@param ― options Configuration options for the test.

@param ― fn The function under test. The first argument to this function is a TestContext object. If the test uses callbacks, the callback function is passed as the second argument.

@return ― Fulfilled with undefined once the test completes, or immediately if the test runs within a suite.

test
} from 'node:test';
import
function assert(value: unknown, message?: string | Error): asserts value
assert
from 'node:assert/strict';
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 'resilience-pipeline';
import {
class FakeTimeProvider

A clock you move by hand, so retries, timeouts and open circuits can be tested without waiting.

const time = new FakeTimeProvider();
const pipeline = new ResiliencePipelineBuilder({ timeProvider: time }).addRetry({ delay: '10s' }).addTimeout('1s').build();
// Attach the expectation first: the promise settles while runAll() fires timers.
const done = assert.rejects(pipeline.execute(() => neverResolves()), TimeoutRejectedError);
await time.runAll(); // 4 timeouts and 3 delays, instantly
await done;

FakeTimeProvider
} from 'resilience-pipeline/testing';
function test(name?: string, fn?: test.TestFn): Promise<void> (+3 overloads)

The test() function is the value imported from the test module. Each invocation of this function results in reporting the test to the TestsStream.

The TestContext object passed to the fn argument can be used to perform actions related to the current test. Examples include skipping the test, adding additional diagnostic information, or creating subtests.

test() returns a Promise that fulfills once the test completes. if test() is called within a suite, it fulfills immediately. The return value can usually be discarded for top level tests. However, the return value from subtests should be used to prevent the parent test from finishing first and cancelling the subtest as shown in the following example.

test('top level test', async (t) => {
// The setTimeout() in the following subtest would cause it to outlive its
// parent test if 'await' is removed on the next line. Once the parent test
// completes, it will cancel any outstanding subtests.
await t.test('longer running subtest', async (t) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
});
});

The timeout option can be used to fail the test if it takes longer than timeout milliseconds to complete. However, it is not a reliable mechanism for canceling tests because a running test might block the application thread and thus prevent the scheduled cancellation.

@since ― v18.0.0, v16.17.0

@param ― name The name of the test, which is displayed when reporting test results. Defaults to the name property of fn, or '<anonymous>' if fn does not have a name.

@param ― options Configuration options for the test.

@param ― fn The function under test. The first argument to this function is a TestContext object. If the test uses callbacks, the callback function is passed as the second argument.

@return ― Fulfilled with undefined once the test completes, or immediately if the test runs within a suite.

test
('retries exactly 10 s apart', async () => {
const
const time: FakeTimeProvider
time
= new
new FakeTimeProvider(start?: number | Date): FakeTimeProvider

A clock you move by hand, so retries, timeouts and open circuits can be tested without waiting.

const time = new FakeTimeProvider();
const pipeline = new ResiliencePipelineBuilder({ timeProvider: time }).addRetry({ delay: '10s' }).addTimeout('1s').build();
// Attach the expectation first: the promise settles while runAll() fires timers.
const done = assert.rejects(pipeline.execute(() => neverResolves()), TimeoutRejectedError);
await time.runAll(); // 4 timeouts and 3 delays, instantly
await done;

FakeTimeProvider
();
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
({
ResiliencePipelineBuilderOptions.timeProvider?: TimeProvider | undefined

Clock and timers for every strategy. Default: the system clock. Use FakeTimeProvider in tests.

timeProvider
:
const time: FakeTimeProvider
time
})
.
ResiliencePipelineBuilder<any, never, never>.addRetry(options?: RetryOptions<any>): ResiliencePipelineBuilder<any, never, never>
addRetry
({
RetryOptions<any>.delay?: Duration | undefined

Base delay. Default 2000 ms.

delay
: '10s',
RetryOptions<any>.maxRetryAttempts?: number | undefined

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

maxRetryAttempts
: 3 })
.
ResiliencePipelineBuilder<any, never, never>.build(): ResiliencePipeline<any, never>
build
();
let
let calls: number
calls
= 0;
const
const done: Promise<string>
done
=
const pipeline: ResiliencePipeline<any, never>
pipeline
.
ResiliencePipeline<any, never>.execute<string>(callback: (context: ResilienceContext) => MaybePromise<string>, options?: ExecuteOptions): Promise<string>

Runs the callback through the pipeline; throws the final error.

execute
(async () => {
if (++
let calls: number
calls
< 3) throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('503 from upstream');
return 'ok';
});
await
const time: FakeTimeProvider
time
.
FakeTimeProvider.advance(duration: Duration): Promise<void>

Moves the clock forward, firing every timer that comes due, in order.

advance
(0); // let the first attempt run
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.equal<1>(actual: unknown, expected: 1, message?: string | Error): asserts actual is 1
export strict.equal

Tests strict equality between the actual and expected parameters as determined by Object.is().

import assert from 'node:assert/strict';
assert.strictEqual(1, 2);
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
//
// 1 !== 2
assert.strictEqual(1, 1);
// OK
assert.strictEqual('Hello foobar', 'Hello World!');
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
// + actual - expected
//
// + 'Hello foobar'
// - 'Hello World!'
// ^
const apples = 1;
const oranges = 2;
assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
// TypeError: Inputs are not identical

If the values are not strictly equal, an AssertionError is thrown with a message property set equal to the value of the message parameter. If the message parameter is undefined, a default error message is assigned. If the message parameter is an instance of an Error then it will be thrown instead of the AssertionError.

@since ― v0.1.21

equal
(
let calls: number
calls
, 1);
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.equal<1>(actual: unknown, expected: 1, message?: string | Error): asserts actual is 1
export strict.equal

Tests strict equality between the actual and expected parameters as determined by Object.is().

import assert from 'node:assert/strict';
assert.strictEqual(1, 2);
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
//
// 1 !== 2
assert.strictEqual(1, 1);
// OK
assert.strictEqual('Hello foobar', 'Hello World!');
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
// + actual - expected
//
// + 'Hello foobar'
// - 'Hello World!'
// ^
const apples = 1;
const oranges = 2;
assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
// TypeError: Inputs are not identical

If the values are not strictly equal, an AssertionError is thrown with a message property set equal to the value of the message parameter. If the message parameter is undefined, a default error message is assigned. If the message parameter is an instance of an Error then it will be thrown instead of the AssertionError.

@since ― v0.1.21

equal
(
const time: FakeTimeProvider
time
.
FakeTimeProvider.pendingTimers: number

Timers scheduled and not yet fired.

pendingTimers
, 1); // the retry delay
await
const time: FakeTimeProvider
time
.
FakeTimeProvider.advance(duration: Duration): Promise<void>

Moves the clock forward, firing every timer that comes due, in order.

advance
('9999ms');
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.equal<1>(actual: unknown, expected: 1, message?: string | Error): asserts actual is 1
export strict.equal

Tests strict equality between the actual and expected parameters as determined by Object.is().

import assert from 'node:assert/strict';
assert.strictEqual(1, 2);
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
//
// 1 !== 2
assert.strictEqual(1, 1);
// OK
assert.strictEqual('Hello foobar', 'Hello World!');
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
// + actual - expected
//
// + 'Hello foobar'
// - 'Hello World!'
// ^
const apples = 1;
const oranges = 2;
assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
// TypeError: Inputs are not identical

If the values are not strictly equal, an AssertionError is thrown with a message property set equal to the value of the message parameter. If the message parameter is undefined, a default error message is assigned. If the message parameter is an instance of an Error then it will be thrown instead of the AssertionError.

@since ― v0.1.21

equal
(
let calls: 1
calls
, 1); // not yet
await
const time: FakeTimeProvider
time
.
FakeTimeProvider.advance(duration: Duration): Promise<void>

Moves the clock forward, firing every timer that comes due, in order.

advance
(1);
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.equal<2>(actual: unknown, expected: 2, message?: string | Error): asserts actual is 2
export strict.equal

Tests strict equality between the actual and expected parameters as determined by Object.is().

import assert from 'node:assert/strict';
assert.strictEqual(1, 2);
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
//
// 1 !== 2
assert.strictEqual(1, 1);
// OK
assert.strictEqual('Hello foobar', 'Hello World!');
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
// + actual - expected
//
// + 'Hello foobar'
// - 'Hello World!'
// ^
const apples = 1;
const oranges = 2;
assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
// TypeError: Inputs are not identical

If the values are not strictly equal, an AssertionError is thrown with a message property set equal to the value of the message parameter. If the message parameter is undefined, a default error message is assigned. If the message parameter is an instance of an Error then it will be thrown instead of the AssertionError.

@since ― v0.1.21

equal
(
let calls: 1
calls
, 2); // exactly at 10 s
await
const time: FakeTimeProvider
time
.
FakeTimeProvider.advance(duration: Duration): Promise<void>

Moves the clock forward, firing every timer that comes due, in order.

advance
('10s');
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.equal<"ok">(actual: unknown, expected: "ok", message?: string | Error): asserts actual is "ok"
export strict.equal

Tests strict equality between the actual and expected parameters as determined by Object.is().

import assert from 'node:assert/strict';
assert.strictEqual(1, 2);
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
//
// 1 !== 2
assert.strictEqual(1, 1);
// OK
assert.strictEqual('Hello foobar', 'Hello World!');
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
// + actual - expected
//
// + 'Hello foobar'
// - 'Hello World!'
// ^
const apples = 1;
const oranges = 2;
assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
// TypeError: Inputs are not identical

If the values are not strictly equal, an AssertionError is thrown with a message property set equal to the value of the message parameter. If the message parameter is undefined, a default error message is assigned. If the message parameter is an instance of an Error then it will be thrown instead of the AssertionError.

@since ― v0.1.21

equal
(await
const done: Promise<string>
done
, 'ok');
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.equal<0>(actual: unknown, expected: 0, message?: string | Error): asserts actual is 0
export strict.equal

Tests strict equality between the actual and expected parameters as determined by Object.is().

import assert from 'node:assert/strict';
assert.strictEqual(1, 2);
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
//
// 1 !== 2
assert.strictEqual(1, 1);
// OK
assert.strictEqual('Hello foobar', 'Hello World!');
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
// + actual - expected
//
// + 'Hello foobar'
// - 'Hello World!'
// ^
const apples = 1;
const oranges = 2;
assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
// TypeError: Inputs are not identical

If the values are not strictly equal, an AssertionError is thrown with a message property set equal to the value of the message parameter. If the message parameter is undefined, a default error message is assigned. If the message parameter is an instance of an Error then it will be thrown instead of the AssertionError.

@since ― v0.1.21

equal
(
const time: FakeTimeProvider
time
.
FakeTimeProvider.pendingTimers: 1

Timers scheduled and not yet fired.

pendingTimers
, 0);
});

Build pipelines in a function that takes the clock, so production code uses the system clock and tests pass a fake one:

payments-pipeline.ts
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
, type
(alias) interface TimeProvider
import TimeProvider

Clock and timers used by every strategy. Swap it for FakeTimeProvider (resilience-pipeline/testing) to test without waiting.

TimeProvider
} from 'resilience-pipeline';
export function
function paymentsPipeline(timeProvider?: TimeProvider): ResiliencePipeline<Response, WithErrors<BrokenCircuitError, never, TimeoutRejectedError>>
paymentsPipeline
(
timeProvider: TimeProvider | undefined
timeProvider
?:
(alias) interface TimeProvider
import TimeProvider

Clock and timers used by every strategy. Swap it for FakeTimeProvider (resilience-pipeline/testing) to test without waiting.

TimeProvider
) {
return 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
>({
ResiliencePipelineBuilderOptions.name?: string | undefined

Labels telemetry events.

name
: 'payments',
ResiliencePipelineBuilderOptions.timeProvider?: TimeProvider | undefined

Clock and timers for every strategy. Default: the system clock. Use FakeTimeProvider in tests.

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

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

maxRetryAttempts
: 3,
RetryOptions<Response>.delay?: Duration | undefined

Base delay. Default 2000 ms.

delay
: '200ms',
RetryOptions<Response>.backoffType?: BackoffType | undefined

Default 'constant'.

backoffType
: 'exponential' })
.
ResiliencePipelineBuilder<Response, never, never>.addCircuitBreaker(options?: CircuitBreakerOptions<Response>): Next<Response, never, never, BrokenCircuitError>
addCircuitBreaker
({
CircuitBreakerOptions<Response>.consecutiveFailures?: number | undefined

Break after this many failures in a row (a success resets the count). Simple and good for low traffic. When set without failureRatio, the ratio check is off and only this rule applies.

consecutiveFailures
: 5,
CircuitBreakerOptions<Response>.breakDuration?: Duration | undefined

How long the circuit stays open. Default 5 s.

breakDuration
: '30s' })
.
ResiliencePipelineBuilder<Response, BrokenCircuitError, never>.addTimeout(options: Duration | TimeoutOptions): Next<Response, BrokenCircuitError, never, TimeoutRejectedError>

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

addTimeout
('2s')
.
ResiliencePipelineBuilder<Response, WithErrors<BrokenCircuitError, never, TimeoutRejectedError>, never>.build(): ResiliencePipeline<Response, WithErrors<BrokenCircuitError, never, TimeoutRejectedError>>
build
();
}

When you can’t rebuild it: a clock per execution

Section titled “When you can’t rebuild it: a clock per execution”

Sometimes the pipeline is a module-level singleton you can’t rebuild in a test. execute(fn, { timeProvider }) swaps the clock for that one call:

// Defined once at module level in your app.
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>.delay?: Duration | undefined

Base delay. Default 2000 ms.

delay
: '5s',
RetryOptions<any>.maxRetryAttempts?: number | undefined

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

maxRetryAttempts
: 2 })
.
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>.build(): ResiliencePipeline<any, TimeoutRejectedError>
build
();
function test(name?: string, fn?: test.TestFn): Promise<void> (+3 overloads)

The test() function is the value imported from the test module. Each invocation of this function results in reporting the test to the TestsStream.

The TestContext object passed to the fn argument can be used to perform actions related to the current test. Examples include skipping the test, adding additional diagnostic information, or creating subtests.

test() returns a Promise that fulfills once the test completes. if test() is called within a suite, it fulfills immediately. The return value can usually be discarded for top level tests. However, the return value from subtests should be used to prevent the parent test from finishing first and cancelling the subtest as shown in the following example.

test('top level test', async (t) => {
// The setTimeout() in the following subtest would cause it to outlive its
// parent test if 'await' is removed on the next line. Once the parent test
// completes, it will cancel any outstanding subtests.
await t.test('longer running subtest', async (t) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
});
});

The timeout option can be used to fail the test if it takes longer than timeout milliseconds to complete. However, it is not a reliable mechanism for canceling tests because a running test might block the application thread and thus prevent the scheduled cancellation.

@since ― v18.0.0, v16.17.0

@param ― name The name of the test, which is displayed when reporting test results. Defaults to the name property of fn, or '<anonymous>' if fn does not have a name.

@param ― options Configuration options for the test.

@param ― fn The function under test. The first argument to this function is a TestContext object. If the test uses callbacks, the callback function is passed as the second argument.

@return ― Fulfilled with undefined once the test completes, or immediately if the test runs within a suite.

test
('times out on a hanging dependency', async () => {
const
const time: FakeTimeProvider
time
= new
new FakeTimeProvider(start?: number | Date): FakeTimeProvider

A clock you move by hand, so retries, timeouts and open circuits can be tested without waiting.

const time = new FakeTimeProvider();
const pipeline = new ResiliencePipelineBuilder({ timeProvider: time }).addRetry({ delay: '10s' }).addTimeout('1s').build();
// Attach the expectation first: the promise settles while runAll() fires timers.
const done = assert.rejects(pipeline.execute(() => neverResolves()), TimeoutRejectedError);
await time.runAll(); // 4 timeouts and 3 delays, instantly
await done;

FakeTimeProvider
();
const
const done: Promise<void>
done
=
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.rejects(block: (() => Promise<unknown>) | Promise<unknown>, error: assert.AssertPredicate, message?: string | Error): Promise<void> (+1 overload)
export strict.rejects

Awaits the asyncFn promise or, if asyncFn is a function, immediately calls the function and awaits the returned promise to complete. It will then check that the promise is rejected.

If asyncFn is a function and it throws an error synchronously, assert.rejects() will return a rejected Promise with that error. If the function does not return a promise, assert.rejects() will return a rejected Promise with an ERR_INVALID_RETURN_VALUE error. In both cases the error handler is skipped.

Besides the async nature to await the completion behaves identically to

throws

.

If specified, error can be a Class, RegExp, a validation function, an object where each property will be tested for, or an instance of error where each property will be tested for including the non-enumerable message and name properties.

If specified, message will be the message provided by the `

AssertionError

if theasyncFn` fails to reject.

import assert from 'node:assert/strict';
await assert.rejects(
async () => {
throw new TypeError('Wrong value');
},
{
name: 'TypeError',
message: 'Wrong value',
},
);
import assert from 'node:assert/strict';
await assert.rejects(
async () => {
throw new TypeError('Wrong value');
},
(err) => {
assert.strictEqual(err.name, 'TypeError');
assert.strictEqual(err.message, 'Wrong value');
return true;
},
);
import assert from 'node:assert/strict';
assert.rejects(
Promise.reject(new Error('Wrong value')),
Error,
).then(() => {
// ...
});

error cannot be a string. If a string is provided as the second argument, then error is assumed to be omitted and the string will be used for message instead. This can lead to easy-to-miss mistakes. Please read the example in

throws

carefully if using a string as the second argument gets considered.

@since ― v10.0.0

rejects
(
const pipeline: ResiliencePipeline<any, TimeoutRejectedError>
pipeline
.
ResiliencePipeline<any, TimeoutRejectedError>.execute<unknown>(callback: (context: ResilienceContext) => unknown, options?: ExecuteOptions): Promise<unknown>

Runs the callback through the pipeline; throws the final error.

execute
(() => new
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>

Creates a new Promise.

@param ― executor A callback used to initialize the promise. This callback is passed two arguments: a resolve callback used to resolve the promise with a value or the result of another promise, and a reject callback used to reject the promise with a provided reason or error.

Promise
(() => {}), {
ExecuteOptions.timeProvider?: TimeProvider | undefined

Clock for this execution's retry delays / timeouts / hedging delays (e.g. a FakeTimeProvider in one test).

timeProvider
:
const time: FakeTimeProvider
time
}),
class TimeoutRejectedError
TimeoutRejectedError
);
await
const time: FakeTimeProvider
time
.
FakeTimeProvider.runAll(limit?: number): Promise<void>

Fires timers until none are left (including ones scheduled while firing). Throws after limit timers to catch infinite loops.

runAll
();
await
const done: Promise<void>
done
;
});

The per-execution clock drives that execution’s own timing only: retry delays, timeouts, hedging delays and chaos latency. Stateful strategies (circuit breakers, rate limiters, caches, locks) keep the pipeline’s clock, because their state spans many executions. To test a breaker’s breakDuration or a limiter’s refill, give the builder the fake clock.

Three tools make breaker tests exact: consecutiveFailures (opens after N failures in a row, no ratio math), CircuitBreakerStateProvider (read the state), and nextTelemetry (wait for an event).

import {
function test(name?: string, fn?: test.TestFn): Promise<void> (+3 overloads)

The test() function is the value imported from the test module. Each invocation of this function results in reporting the test to the TestsStream.

The TestContext object passed to the fn argument can be used to perform actions related to the current test. Examples include skipping the test, adding additional diagnostic information, or creating subtests.

test() returns a Promise that fulfills once the test completes. if test() is called within a suite, it fulfills immediately. The return value can usually be discarded for top level tests. However, the return value from subtests should be used to prevent the parent test from finishing first and cancelling the subtest as shown in the following example.

test('top level test', async (t) => {
// The setTimeout() in the following subtest would cause it to outlive its
// parent test if 'await' is removed on the next line. Once the parent test
// completes, it will cancel any outstanding subtests.
await t.test('longer running subtest', async (t) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
});
});

The timeout option can be used to fail the test if it takes longer than timeout milliseconds to complete. However, it is not a reliable mechanism for canceling tests because a running test might block the application thread and thus prevent the scheduled cancellation.

@since ― v18.0.0, v16.17.0

@param ― name The name of the test, which is displayed when reporting test results. Defaults to the name property of fn, or '<anonymous>' if fn does not have a name.

@param ― options Configuration options for the test.

@param ― fn The function under test. The first argument to this function is a TestContext object. If the test uses callbacks, the callback function is passed as the second argument.

@return ― Fulfilled with undefined once the test completes, or immediately if the test runs within a suite.

test
} from 'node:test';
import
function assert(value: unknown, message?: string | Error): asserts value
assert
from 'node:assert/strict';
import {
class BrokenCircuitError
BrokenCircuitError
,
class CircuitBreakerStateProvider

Reads the state and window metrics of a single circuit breaker (health checks, dashboards, persistence).

CircuitBreakerStateProvider
,
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
,
function nextTelemetry(filter?: (event: TelemetryEvent) => boolean, signal?: AbortSignal): Promise<TelemetryEvent>

Resolves with the next telemetry event matching filter (e.g. in tests: await nextTelemetry((e) => e.event === 'OnCircuitOpened')).

nextTelemetry
} from 'resilience-pipeline';
import {
class FakeTimeProvider

A clock you move by hand, so retries, timeouts and open circuits can be tested without waiting.

const time = new FakeTimeProvider();
const pipeline = new ResiliencePipelineBuilder({ timeProvider: time }).addRetry({ delay: '10s' }).addTimeout('1s').build();
// Attach the expectation first: the promise settles while runAll() fires timers.
const done = assert.rejects(pipeline.execute(() => neverResolves()), TimeoutRejectedError);
await time.runAll(); // 4 timeouts and 3 delays, instantly
await done;

FakeTimeProvider
} from 'resilience-pipeline/testing';
function test(name?: string, fn?: test.TestFn): Promise<void> (+3 overloads)

The test() function is the value imported from the test module. Each invocation of this function results in reporting the test to the TestsStream.

The TestContext object passed to the fn argument can be used to perform actions related to the current test. Examples include skipping the test, adding additional diagnostic information, or creating subtests.

test() returns a Promise that fulfills once the test completes. if test() is called within a suite, it fulfills immediately. The return value can usually be discarded for top level tests. However, the return value from subtests should be used to prevent the parent test from finishing first and cancelling the subtest as shown in the following example.

test('top level test', async (t) => {
// The setTimeout() in the following subtest would cause it to outlive its
// parent test if 'await' is removed on the next line. Once the parent test
// completes, it will cancel any outstanding subtests.
await t.test('longer running subtest', async (t) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
});
});

The timeout option can be used to fail the test if it takes longer than timeout milliseconds to complete. However, it is not a reliable mechanism for canceling tests because a running test might block the application thread and thus prevent the scheduled cancellation.

@since ― v18.0.0, v16.17.0

@param ― name The name of the test, which is displayed when reporting test results. Defaults to the name property of fn, or '<anonymous>' if fn does not have a name.

@param ― options Configuration options for the test.

@param ― fn The function under test. The first argument to this function is a TestContext object. If the test uses callbacks, the callback function is passed as the second argument.

@return ― Fulfilled with undefined once the test completes, or immediately if the test runs within a suite.

test
('opens after 3 failures, probes after 30 s, closes on success', async () => {
const
const time: FakeTimeProvider
time
= new
new FakeTimeProvider(start?: number | Date): FakeTimeProvider

A clock you move by hand, so retries, timeouts and open circuits can be tested without waiting.

const time = new FakeTimeProvider();
const pipeline = new ResiliencePipelineBuilder({ timeProvider: time }).addRetry({ delay: '10s' }).addTimeout('1s').build();
// Attach the expectation first: the promise settles while runAll() fires timers.
const done = assert.rejects(pipeline.execute(() => neverResolves()), TimeoutRejectedError);
await time.runAll(); // 4 timeouts and 3 delays, instantly
await done;

FakeTimeProvider
();
const
const state: CircuitBreakerStateProvider
state
= new
new CircuitBreakerStateProvider(): CircuitBreakerStateProvider

Reads the state and window metrics of a single circuit breaker (health checks, dashboards, persistence).

CircuitBreakerStateProvider
();
const
const pipeline: ResiliencePipeline<any, BrokenCircuitError>
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
({
ResiliencePipelineBuilderOptions.name?: string | undefined

Labels telemetry events.

name
: 'payments',
ResiliencePipelineBuilderOptions.timeProvider?: TimeProvider | undefined

Clock and timers for every strategy. Default: the system clock. Use FakeTimeProvider in tests.

timeProvider
:
const time: FakeTimeProvider
time
})
.
ResiliencePipelineBuilder<any, never, never>.addCircuitBreaker(options?: CircuitBreakerOptions<any>): Next<any, never, never, BrokenCircuitError>
addCircuitBreaker
({
CircuitBreakerOptions<any>.consecutiveFailures?: number | undefined

Break after this many failures in a row (a success resets the count). Simple and good for low traffic. When set without failureRatio, the ratio check is off and only this rule applies.

consecutiveFailures
: 3,
CircuitBreakerOptions<any>.breakDuration?: Duration | undefined

How long the circuit stays open. Default 5 s.

breakDuration
: '30s',
CircuitBreakerOptions<any>.stateProvider?: CircuitBreakerStateProvider | undefined
stateProvider
:
const state: CircuitBreakerStateProvider
state
})
.
ResiliencePipelineBuilder<any, BrokenCircuitError, never>.build(): ResiliencePipeline<any, BrokenCircuitError>
build
();
// Subscribe before the action that emits the event.
const
const opened: Promise<TelemetryEvent>
opened
=
function nextTelemetry(filter?: (event: TelemetryEvent) => boolean, signal?: AbortSignal): Promise<TelemetryEvent>

Resolves with the next telemetry event matching filter (e.g. in tests: await nextTelemetry((e) => e.event === 'OnCircuitOpened')).

nextTelemetry
((
e: TelemetryEvent
e
) =>
e: TelemetryEvent
e
.
TelemetryEvent.event: string
event
=== 'OnCircuitOpened' &&
e: TelemetryEvent
e
.
TelemetryEvent.source: TelemetrySource
source
.
TelemetrySource.pipelineName: string | undefined
pipelineName
=== 'payments');
const
const failing: () => Promise<never>
failing
= () =>
var Promise: PromiseConstructor

Represents the completion of an asynchronous operation

Promise
.
PromiseConstructor.reject<never>(reason?: any): Promise<never>

Creates a new rejected promise for the provided reason.

@param ― reason The reason the promise was rejected.

@returns ― A new rejected Promise.

reject
(new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('connection reset'));
for (let
let i: number
i
= 0;
let i: number
i
< 3;
let i: number
i
++) await
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.rejects(block: (() => Promise<unknown>) | Promise<unknown>, error: assert.AssertPredicate, message?: string | Error): Promise<void> (+1 overload)
export strict.rejects

Awaits the asyncFn promise or, if asyncFn is a function, immediately calls the function and awaits the returned promise to complete. It will then check that the promise is rejected.

If asyncFn is a function and it throws an error synchronously, assert.rejects() will return a rejected Promise with that error. If the function does not return a promise, assert.rejects() will return a rejected Promise with an ERR_INVALID_RETURN_VALUE error. In both cases the error handler is skipped.

Besides the async nature to await the completion behaves identically to

throws

.

If specified, error can be a Class, RegExp, a validation function, an object where each property will be tested for, or an instance of error where each property will be tested for including the non-enumerable message and name properties.

If specified, message will be the message provided by the `

AssertionError

if theasyncFn` fails to reject.

import assert from 'node:assert/strict';
await assert.rejects(
async () => {
throw new TypeError('Wrong value');
},
{
name: 'TypeError',
message: 'Wrong value',
},
);
import assert from 'node:assert/strict';
await assert.rejects(
async () => {
throw new TypeError('Wrong value');
},
(err) => {
assert.strictEqual(err.name, 'TypeError');
assert.strictEqual(err.message, 'Wrong value');
return true;
},
);
import assert from 'node:assert/strict';
assert.rejects(
Promise.reject(new Error('Wrong value')),
Error,
).then(() => {
// ...
});

error cannot be a string. If a string is provided as the second argument, then error is assumed to be omitted and the string will be used for message instead. This can lead to easy-to-miss mistakes. Please read the example in

throws

carefully if using a string as the second argument gets considered.

@since ― v10.0.0

rejects
(
const pipeline: ResiliencePipeline<any, BrokenCircuitError>
pipeline
.
ResiliencePipeline<any, BrokenCircuitError>.execute<never>(callback: (context: ResilienceContext) => Promise<never>, options?: ExecuteOptions): Promise<never>

Runs the callback through the pipeline; throws the final error.

execute
(
const failing: () => Promise<never>
failing
), /connection reset/);
await
const opened: Promise<TelemetryEvent>
opened
;
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.equal<"open">(actual: unknown, expected: "open", message?: string | Error): asserts actual is "open"
export strict.equal

Tests strict equality between the actual and expected parameters as determined by Object.is().

import assert from 'node:assert/strict';
assert.strictEqual(1, 2);
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
//
// 1 !== 2
assert.strictEqual(1, 1);
// OK
assert.strictEqual('Hello foobar', 'Hello World!');
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
// + actual - expected
//
// + 'Hello foobar'
// - 'Hello World!'
// ^
const apples = 1;
const oranges = 2;
assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
// TypeError: Inputs are not identical

If the values are not strictly equal, an AssertionError is thrown with a message property set equal to the value of the message parameter. If the message parameter is undefined, a default error message is assigned. If the message parameter is an instance of an Error then it will be thrown instead of the AssertionError.

@since ― v0.1.21

equal
(
const state: CircuitBreakerStateProvider
state
.
CircuitBreakerStateProvider.circuitState: CircuitState
circuitState
, 'open');
// While open, calls are refused without reaching the dependency.
const
const refused: unknown
refused
= await
const pipeline: ResiliencePipeline<any, BrokenCircuitError>
pipeline
.
ResiliencePipeline<any, BrokenCircuitError>.execute<string>(callback: (context: ResilienceContext) => MaybePromise<string>, options?: ExecuteOptions): Promise<string>

Runs the callback through the pipeline; throws the final error.

execute
(() => 'never runs').
Promise<string>.catch<unknown>(onrejected?: ((reason: any) => unknown) | null | undefined): Promise<unknown>

Attaches a callback for only the rejection of the Promise.

@param ― onrejected The callback to execute when the Promise is rejected.

@returns ― A Promise for the completion of the callback.

catch
((
e: unknown
e
: unknown) =>
e: unknown
e
);
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.ok(value: unknown, message?: string | Error): asserts value
export strict.ok

Tests if value is truthy. It is equivalent to assert.equal(!!value, true, message).

If value is not truthy, an AssertionError is thrown with a message property set equal to the value of the message parameter. If the message parameter is undefined, a default error message is assigned. If the message parameter is an instance of an Error then it will be thrown instead of the AssertionError. If no arguments are passed in at all message will be set to the string:'No value argument passed to `assert.ok()`'.

Be aware that in the repl the error message will be different to the one thrown in a file! See below for further details.

import assert from 'node:assert/strict';
assert.ok(true);
// OK
assert.ok(1);
// OK
assert.ok();
// AssertionError: No value argument passed to `assert.ok()`
assert.ok(false, 'it\'s false');
// AssertionError: it's false
// In the repl:
assert.ok(typeof 123 === 'string');
// AssertionError: false == true
// In a file (e.g. test.js):
assert.ok(typeof 123 === 'string');
// AssertionError: The expression evaluated to a falsy value:
//
// assert.ok(typeof 123 === 'string')
assert.ok(false);
// AssertionError: The expression evaluated to a falsy value:
//
// assert.ok(false)
assert.ok(0);
// AssertionError: The expression evaluated to a falsy value:
//
// assert.ok(0)
import assert from 'node:assert/strict';
// Using `assert()` works the same:
assert(0);
// AssertionError: The expression evaluated to a falsy value:
//
// assert(0)

@since ― v0.1.21

ok
(
const refused: unknown
refused
instanceof
class BrokenCircuitError
BrokenCircuitError
);
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.equal<30000>(actual: unknown, expected: 30000, message?: string | Error): asserts actual is 30000
export strict.equal

Tests strict equality between the actual and expected parameters as determined by Object.is().

import assert from 'node:assert/strict';
assert.strictEqual(1, 2);
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
//
// 1 !== 2
assert.strictEqual(1, 1);
// OK
assert.strictEqual('Hello foobar', 'Hello World!');
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
// + actual - expected
//
// + 'Hello foobar'
// - 'Hello World!'
// ^
const apples = 1;
const oranges = 2;
assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
// TypeError: Inputs are not identical

If the values are not strictly equal, an AssertionError is thrown with a message property set equal to the value of the message parameter. If the message parameter is undefined, a default error message is assigned. If the message parameter is an instance of an Error then it will be thrown instead of the AssertionError.

@since ― v0.1.21

equal
(
const refused: BrokenCircuitError
refused
.
BrokenCircuitError.retryAfter: number | undefined

Milliseconds until the circuit allows a probe call.

retryAfter
, 30_000);
// After the break, the next call is the half-open probe.
await
const time: FakeTimeProvider
time
.
FakeTimeProvider.advance(duration: Duration): Promise<void>

Moves the clock forward, firing every timer that comes due, in order.

advance
('30s');
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.equal<"probe">(actual: unknown, expected: "probe", message?: string | Error): asserts actual is "probe"
export strict.equal

Tests strict equality between the actual and expected parameters as determined by Object.is().

import assert from 'node:assert/strict';
assert.strictEqual(1, 2);
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
//
// 1 !== 2
assert.strictEqual(1, 1);
// OK
assert.strictEqual('Hello foobar', 'Hello World!');
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
// + actual - expected
//
// + 'Hello foobar'
// - 'Hello World!'
// ^
const apples = 1;
const oranges = 2;
assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
// TypeError: Inputs are not identical

If the values are not strictly equal, an AssertionError is thrown with a message property set equal to the value of the message parameter. If the message parameter is undefined, a default error message is assigned. If the message parameter is an instance of an Error then it will be thrown instead of the AssertionError.

@since ― v0.1.21

equal
(await
const pipeline: ResiliencePipeline<any, BrokenCircuitError>
pipeline
.
ResiliencePipeline<any, BrokenCircuitError>.execute<string>(callback: (context: ResilienceContext) => MaybePromise<string>, options?: ExecuteOptions): Promise<string>

Runs the callback through the pipeline; throws the final error.

execute
(() => 'probe'), 'probe');
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.equal<"closed">(actual: unknown, expected: "closed", message?: string | Error): asserts actual is "closed"
export strict.equal

Tests strict equality between the actual and expected parameters as determined by Object.is().

import assert from 'node:assert/strict';
assert.strictEqual(1, 2);
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
//
// 1 !== 2
assert.strictEqual(1, 1);
// OK
assert.strictEqual('Hello foobar', 'Hello World!');
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
// + actual - expected
//
// + 'Hello foobar'
// - 'Hello World!'
// ^
const apples = 1;
const oranges = 2;
assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
// TypeError: Inputs are not identical

If the values are not strictly equal, an AssertionError is thrown with a message property set equal to the value of the message parameter. If the message parameter is undefined, a default error message is assigned. If the message parameter is an instance of an Error then it will be thrown instead of the AssertionError.

@since ― v0.1.21

equal
(
const state: CircuitBreakerStateProvider
state
.
CircuitBreakerStateProvider.circuitState: "open"
circuitState
, 'closed');
});

To start a test from a given state, use the live strategy: pipeline.getStrategy<CircuitBreakerStrategy<any>>('CircuitBreaker') has trip() (open for the break duration) and reset() (close and clear the window).

Limiters read the builder’s clock too, so refills and queueTimeout queues are exact:

import {
function test(name?: string, fn?: test.TestFn): Promise<void> (+3 overloads)

The test() function is the value imported from the test module. Each invocation of this function results in reporting the test to the TestsStream.

The TestContext object passed to the fn argument can be used to perform actions related to the current test. Examples include skipping the test, adding additional diagnostic information, or creating subtests.

test() returns a Promise that fulfills once the test completes. if test() is called within a suite, it fulfills immediately. The return value can usually be discarded for top level tests. However, the return value from subtests should be used to prevent the parent test from finishing first and cancelling the subtest as shown in the following example.

test('top level test', async (t) => {
// The setTimeout() in the following subtest would cause it to outlive its
// parent test if 'await' is removed on the next line. Once the parent test
// completes, it will cancel any outstanding subtests.
await t.test('longer running subtest', async (t) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
});
});

The timeout option can be used to fail the test if it takes longer than timeout milliseconds to complete. However, it is not a reliable mechanism for canceling tests because a running test might block the application thread and thus prevent the scheduled cancellation.

@since ― v18.0.0, v16.17.0

@param ― name The name of the test, which is displayed when reporting test results. Defaults to the name property of fn, or '<anonymous>' if fn does not have a name.

@param ― options Configuration options for the test.

@param ― fn The function under test. The first argument to this function is a TestContext object. If the test uses callbacks, the callback function is passed as the second argument.

@return ― Fulfilled with undefined once the test completes, or immediately if the test runs within a suite.

test
} from 'node:test';
import
function assert(value: unknown, message?: string | Error): asserts value
assert
from 'node:assert/strict';
import {
class RateLimiterRejectedError
RateLimiterRejectedError
,
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 'resilience-pipeline';
import {
class FakeTimeProvider

A clock you move by hand, so retries, timeouts and open circuits can be tested without waiting.

const time = new FakeTimeProvider();
const pipeline = new ResiliencePipelineBuilder({ timeProvider: time }).addRetry({ delay: '10s' }).addTimeout('1s').build();
// Attach the expectation first: the promise settles while runAll() fires timers.
const done = assert.rejects(pipeline.execute(() => neverResolves()), TimeoutRejectedError);
await time.runAll(); // 4 timeouts and 3 delays, instantly
await done;

FakeTimeProvider
} from 'resilience-pipeline/testing';
function test(name?: string, fn?: test.TestFn): Promise<void> (+3 overloads)

The test() function is the value imported from the test module. Each invocation of this function results in reporting the test to the TestsStream.

The TestContext object passed to the fn argument can be used to perform actions related to the current test. Examples include skipping the test, adding additional diagnostic information, or creating subtests.

test() returns a Promise that fulfills once the test completes. if test() is called within a suite, it fulfills immediately. The return value can usually be discarded for top level tests. However, the return value from subtests should be used to prevent the parent test from finishing first and cancelling the subtest as shown in the following example.

test('top level test', async (t) => {
// The setTimeout() in the following subtest would cause it to outlive its
// parent test if 'await' is removed on the next line. Once the parent test
// completes, it will cancel any outstanding subtests.
await t.test('longer running subtest', async (t) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
});
});

The timeout option can be used to fail the test if it takes longer than timeout milliseconds to complete. However, it is not a reliable mechanism for canceling tests because a running test might block the application thread and thus prevent the scheduled cancellation.

@since ― v18.0.0, v16.17.0

@param ― name The name of the test, which is displayed when reporting test results. Defaults to the name property of fn, or '<anonymous>' if fn does not have a name.

@param ― options Configuration options for the test.

@param ― fn The function under test. The first argument to this function is a TestContext object. If the test uses callbacks, the callback function is passed as the second argument.

@return ― Fulfilled with undefined once the test completes, or immediately if the test runs within a suite.

test
('2 tokens, then one per second; waits up to 500 ms', async () => {
const
const time: FakeTimeProvider
time
= new
new FakeTimeProvider(start?: number | Date): FakeTimeProvider

A clock you move by hand, so retries, timeouts and open circuits can be tested without waiting.

const time = new FakeTimeProvider();
const pipeline = new ResiliencePipelineBuilder({ timeProvider: time }).addRetry({ delay: '10s' }).addTimeout('1s').build();
// Attach the expectation first: the promise settles while runAll() fires timers.
const done = assert.rejects(pipeline.execute(() => neverResolves()), TimeoutRejectedError);
await time.runAll(); // 4 timeouts and 3 delays, instantly
await done;

FakeTimeProvider
();
const
const pipeline: ResiliencePipeline<any, RateLimiterRejectedError>
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
({
ResiliencePipelineBuilderOptions.timeProvider?: TimeProvider | undefined

Clock and timers for every strategy. Default: the system clock. Use FakeTimeProvider in tests.

timeProvider
:
const time: FakeTimeProvider
time
})
.
ResiliencePipelineBuilder<any, never, never>.addRateLimiter(options: TokenBucketOptions): Next<any, never, never, RateLimiterRejectedError>

Token bucket rate limiter.

addRateLimiter
({
TokenBucketOptions.tokenLimit: number

Bucket size (largest burst), and the number of tokens it starts with.

tokenLimit
: 2,
TokenBucketOptions.replenishmentPeriod: Duration

Refill interval.

replenishmentPeriod
: '1s',
TokenBucketOptions.tokensPerPeriod?: number | undefined

Tokens added per period. Default tokenLimit.

tokensPerPeriod
: 1,
PermitRateLimiterOptions.queueTimeout?: Duration | undefined

Longest an execution may wait for permits before it is rejected. Waiters are served first come, first served. Default 0: reject right away.

queueTimeout
: '500ms' })
.
ResiliencePipelineBuilder<any, RateLimiterRejectedError, never>.build(): ResiliencePipeline<any, RateLimiterRejectedError>
build
();
await
const pipeline: ResiliencePipeline<any, RateLimiterRejectedError>
pipeline
.
ResiliencePipeline<any, RateLimiterRejectedError>.execute<number>(callback: (context: ResilienceContext) => MaybePromise<number>, options?: ExecuteOptions): Promise<number>

Runs the callback through the pipeline; throws the final error.

execute
(() => 1);
await
const pipeline: ResiliencePipeline<any, RateLimiterRejectedError>
pipeline
.
ResiliencePipeline<any, RateLimiterRejectedError>.execute<number>(callback: (context: ResilienceContext) => MaybePromise<number>, options?: ExecuteOptions): Promise<number>

Runs the callback through the pipeline; throws the final error.

execute
(() => 2);
// The next token is 1 s away, more than queueTimeout: rejected at once, with a hint.
const
const rejected: unknown
rejected
= await
const pipeline: ResiliencePipeline<any, RateLimiterRejectedError>
pipeline
.
ResiliencePipeline<any, RateLimiterRejectedError>.execute<number>(callback: (context: ResilienceContext) => MaybePromise<number>, options?: ExecuteOptions): Promise<number>

Runs the callback through the pipeline; throws the final error.

execute
(() => 3).
Promise<number>.catch<unknown>(onrejected?: ((reason: any) => unknown) | null | undefined): Promise<unknown>

Attaches a callback for only the rejection of the Promise.

@param ― onrejected The callback to execute when the Promise is rejected.

@returns ― A Promise for the completion of the callback.

catch
((
e: unknown
e
: unknown) =>
e: unknown
e
);
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.ok(value: unknown, message?: string | Error): asserts value
export strict.ok

Tests if value is truthy. It is equivalent to assert.equal(!!value, true, message).

If value is not truthy, an AssertionError is thrown with a message property set equal to the value of the message parameter. If the message parameter is undefined, a default error message is assigned. If the message parameter is an instance of an Error then it will be thrown instead of the AssertionError. If no arguments are passed in at all message will be set to the string:'No value argument passed to `assert.ok()`'.

Be aware that in the repl the error message will be different to the one thrown in a file! See below for further details.

import assert from 'node:assert/strict';
assert.ok(true);
// OK
assert.ok(1);
// OK
assert.ok();
// AssertionError: No value argument passed to `assert.ok()`
assert.ok(false, 'it\'s false');
// AssertionError: it's false
// In the repl:
assert.ok(typeof 123 === 'string');
// AssertionError: false == true
// In a file (e.g. test.js):
assert.ok(typeof 123 === 'string');
// AssertionError: The expression evaluated to a falsy value:
//
// assert.ok(typeof 123 === 'string')
assert.ok(false);
// AssertionError: The expression evaluated to a falsy value:
//
// assert.ok(false)
assert.ok(0);
// AssertionError: The expression evaluated to a falsy value:
//
// assert.ok(0)
import assert from 'node:assert/strict';
// Using `assert()` works the same:
assert(0);
// AssertionError: The expression evaluated to a falsy value:
//
// assert(0)

@since ― v0.1.21

ok
(
const rejected: unknown
rejected
instanceof
class RateLimiterRejectedError
RateLimiterRejectedError
);
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.equal<1000>(actual: unknown, expected: 1000, message?: string | Error): asserts actual is 1000
export strict.equal

Tests strict equality between the actual and expected parameters as determined by Object.is().

import assert from 'node:assert/strict';
assert.strictEqual(1, 2);
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
//
// 1 !== 2
assert.strictEqual(1, 1);
// OK
assert.strictEqual('Hello foobar', 'Hello World!');
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
// + actual - expected
//
// + 'Hello foobar'
// - 'Hello World!'
// ^
const apples = 1;
const oranges = 2;
assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
// TypeError: Inputs are not identical

If the values are not strictly equal, an AssertionError is thrown with a message property set equal to the value of the message parameter. If the message parameter is undefined, a default error message is assigned. If the message parameter is an instance of an Error then it will be thrown instead of the AssertionError.

@since ― v0.1.21

equal
(
const rejected: RateLimiterRejectedError
rejected
.
RateLimiterRejectedError.retryAfter: number | undefined
retryAfter
, 1000);
// 600 ms later the token is 400 ms away: within queueTimeout, so the call waits for it.
await
const time: FakeTimeProvider
time
.
FakeTimeProvider.advance(duration: Duration): Promise<void>

Moves the clock forward, firing every timer that comes due, in order.

advance
('600ms');
const
const waited: Promise<number>
waited
=
const pipeline: ResiliencePipeline<any, RateLimiterRejectedError>
pipeline
.
ResiliencePipeline<any, RateLimiterRejectedError>.execute<number>(callback: (context: ResilienceContext) => MaybePromise<number>, options?: ExecuteOptions): Promise<number>

Runs the callback through the pipeline; throws the final error.

execute
(() => 4);
await
const time: FakeTimeProvider
time
.
FakeTimeProvider.advance(duration: Duration): Promise<void>

Moves the clock forward, firing every timer that comes due, in order.

advance
('400ms');
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.equal<4>(actual: unknown, expected: 4, message?: string | Error): asserts actual is 4
export strict.equal

Tests strict equality between the actual and expected parameters as determined by Object.is().

import assert from 'node:assert/strict';
assert.strictEqual(1, 2);
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
//
// 1 !== 2
assert.strictEqual(1, 1);
// OK
assert.strictEqual('Hello foobar', 'Hello World!');
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
// + actual - expected
//
// + 'Hello foobar'
// - 'Hello World!'
// ^
const apples = 1;
const oranges = 2;
assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
// TypeError: Inputs are not identical

If the values are not strictly equal, an AssertionError is thrown with a message property set equal to the value of the message parameter. If the message parameter is undefined, a default error message is assigned. If the message parameter is an instance of an Error then it will be thrown instead of the AssertionError.

@since ― v0.1.21

equal
(await
const waited: Promise<number>
waited
, 4);
});

A few objects live outside a pipeline and take their own timeProvider. Pass the same fake clock to each:

Object Option
new RetryBudget({ ... }) timeProvider
new MemoryLockStore({ ... }) timeProvider (leases expire on it)
createStats({ ... }) timeProvider (window buckets)
waitFor(fn, { ... }), createBatcher({ ... }) timeProvider
createResilientFetch({ ... }), attachResilience(axios, { ... }) timeProvider

Jittered delays are random. For exact timings, pass randomizer: () => 0.5 to addRetry (or turn jitter off in the test).

Chaos in tests: a failing dependency without mocks

Section titled “Chaos in tests: a failing dependency without mocks”

Chaos strategies make a pipeline fail on purpose. In a test, set injectionRate: 1 and the failure is certain. Write the production configuration as a function that receives a builder, then append the chaos strategy last, so it sits where the real dependency call is and every strategy above it reacts to it:

// Production configuration, shared by the app and the tests.
export const
const configurePricing: (b: ResiliencePipelineBuilder<number>) => Next<number, never, string, TimeoutRejectedError>
configurePricing
= (
b: ResiliencePipelineBuilder<number, never, never>
b
:
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
<number>) =>
b: ResiliencePipelineBuilder<number, never, never>
b
.
ResiliencePipelineBuilder<number, never, never>.addFallback(options: FallbackOptions<number> & {
shouldHandle?: undefined;
}): ResiliencePipelineBuilder<number, never, string> (+1 overload)

Without shouldHandle it catches every failure of the strategies added after it, so their errors leave the error type.

addFallback
({
FallbackOptions<number>.fallbackValue?: number | undefined

A fixed substitute value (cockatiel's fallback(policy, value)).

fallbackValue
: -1 }) // "price unavailable"
.
ResiliencePipelineBuilder<number, never, string>.addRetry(options?: RetryOptions<number>): ResiliencePipelineBuilder<number, never, string>
addRetry
({
RetryOptions<number>.maxRetryAttempts?: number | undefined

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

maxRetryAttempts
: 2,
RetryOptions<number>.delay?: Duration | undefined

Base delay. Default 2000 ms.

delay
: '1s' })
.
ResiliencePipelineBuilder<number, never, string>.addTimeout(options: Duration | TimeoutOptions): Next<number, never, string, TimeoutRejectedError>

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

addTimeout
('500ms');
function test(name?: string, fn?: test.TestFn): Promise<void> (+3 overloads)

The test() function is the value imported from the test module. Each invocation of this function results in reporting the test to the TestsStream.

The TestContext object passed to the fn argument can be used to perform actions related to the current test. Examples include skipping the test, adding additional diagnostic information, or creating subtests.

test() returns a Promise that fulfills once the test completes. if test() is called within a suite, it fulfills immediately. The return value can usually be discarded for top level tests. However, the return value from subtests should be used to prevent the parent test from finishing first and cancelling the subtest as shown in the following example.

test('top level test', async (t) => {
// The setTimeout() in the following subtest would cause it to outlive its
// parent test if 'await' is removed on the next line. Once the parent test
// completes, it will cancel any outstanding subtests.
await t.test('longer running subtest', async (t) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
});
});

The timeout option can be used to fail the test if it takes longer than timeout milliseconds to complete. However, it is not a reliable mechanism for canceling tests because a running test might block the application thread and thus prevent the scheduled cancellation.

@since ― v18.0.0, v16.17.0

@param ― name The name of the test, which is displayed when reporting test results. Defaults to the name property of fn, or '<anonymous>' if fn does not have a name.

@param ― options Configuration options for the test.

@param ― fn The function under test. The first argument to this function is a TestContext object. If the test uses callbacks, the callback function is passed as the second argument.

@return ― Fulfilled with undefined once the test completes, or immediately if the test runs within a suite.

test
('serves the fallback when the pricing service is down', async () => {
const
const time: FakeTimeProvider
time
= new
new FakeTimeProvider(start?: number | Date): FakeTimeProvider

A clock you move by hand, so retries, timeouts and open circuits can be tested without waiting.

const time = new FakeTimeProvider();
const pipeline = new ResiliencePipelineBuilder({ timeProvider: time }).addRetry({ delay: '10s' }).addTimeout('1s').build();
// Attach the expectation first: the promise settles while runAll() fires timers.
const done = assert.rejects(pipeline.execute(() => neverResolves()), TimeoutRejectedError);
await time.runAll(); // 4 timeouts and 3 delays, instantly
await done;

FakeTimeProvider
();
let
let reachedDependency: number
reachedDependency
= 0;
const
const pipeline: ResiliencePipeline<number, never>
pipeline
=
const configurePricing: (b: ResiliencePipelineBuilder<number>) => Next<number, never, string, TimeoutRejectedError>
configurePricing
(new
new ResiliencePipelineBuilder<number, never, never>(options?: ResiliencePipelineBuilderOptions): ResiliencePipelineBuilder<number, 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
<number>({
ResiliencePipelineBuilderOptions.timeProvider?: TimeProvider | undefined

Clock and timers for every strategy. Default: the system clock. Use FakeTimeProvider in tests.

timeProvider
:
const time: FakeTimeProvider
time
}))
.
ResiliencePipelineBuilder<number, never, string>.addChaosFault(options: ChaosFaultOptions): Next<number, never, string, TimeoutRejectedError>
addChaosFault
({
ChaosOptions.injectionRate?: number | undefined

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

injectionRate
: 1,
ChaosFaultOptions.faultGenerator?: ((args: ContextArgs) => MaybePromise<unknown>) | undefined
faultGenerator
: () => new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('pricing: 503') }) // innermost
.
ResiliencePipelineBuilder<number, never, string>.build(): ResiliencePipeline<number, never>
build
();
const
const price: Promise<number>
price
=
const pipeline: ResiliencePipeline<number, never>
pipeline
.
ResiliencePipeline<number, never>.execute<number>(callback: (context: ResilienceContext) => MaybePromise<number>, options?: ExecuteOptions): Promise<number>

Runs the callback through the pipeline; throws the final error.

execute
(({
signal: AbortSignal

Aborted when the caller cancels, or when a strategy (timeout, hedging) gives up on this execution.

signal
}) => (
let reachedDependency: number
reachedDependency
++,
function fetchPrice(sku: string, signal: AbortSignal): Promise<number>
fetchPrice
('sku-1',
signal: AbortSignal

Aborted when the caller cancels, or when a strategy (timeout, hedging) gives up on this execution.

signal
)));
await
const time: FakeTimeProvider
time
.
FakeTimeProvider.runAll(limit?: number): Promise<void>

Fires timers until none are left (including ones scheduled while firing). Throws after limit timers to catch infinite loops.

runAll
();
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.equal<-1>(actual: unknown, expected: -1, message?: string | Error): asserts actual is -1
export strict.equal

Tests strict equality between the actual and expected parameters as determined by Object.is().

import assert from 'node:assert/strict';
assert.strictEqual(1, 2);
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
//
// 1 !== 2
assert.strictEqual(1, 1);
// OK
assert.strictEqual('Hello foobar', 'Hello World!');
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
// + actual - expected
//
// + 'Hello foobar'
// - 'Hello World!'
// ^
const apples = 1;
const oranges = 2;
assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
// TypeError: Inputs are not identical

If the values are not strictly equal, an AssertionError is thrown with a message property set equal to the value of the message parameter. If the message parameter is undefined, a default error message is assigned. If the message parameter is an instance of an Error then it will be thrown instead of the AssertionError.

@since ― v0.1.21

equal
(await
const price: Promise<number>
price
, -1);
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.equal<0>(actual: unknown, expected: 0, message?: string | Error): asserts actual is 0
export strict.equal

Tests strict equality between the actual and expected parameters as determined by Object.is().

import assert from 'node:assert/strict';
assert.strictEqual(1, 2);
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
//
// 1 !== 2
assert.strictEqual(1, 1);
// OK
assert.strictEqual('Hello foobar', 'Hello World!');
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
// + actual - expected
//
// + 'Hello foobar'
// - 'Hello World!'
// ^
const apples = 1;
const oranges = 2;
assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
// TypeError: Inputs are not identical

If the values are not strictly equal, an AssertionError is thrown with a message property set equal to the value of the message parameter. If the message parameter is undefined, a default error message is assigned. If the message parameter is an instance of an Error then it will be thrown instead of the AssertionError.

@since ― v0.1.21

equal
(
let reachedDependency: number
reachedDependency
, 0); // every attempt was intercepted
});

Use addChaosLatency({ injectionRate: 1, latency: '5s' }) the same way to prove a timeout fires, and addChaosOutcome({ injectionRate: 1, outcome: Outcome.fromResult(new Response(null, { status: 503 })) }) to fake a response. For partial failure, keep the rate below 1 and pass a seeded randomizer so the test is repeatable.

setGlobalChaos() also works in tests, but it is one hook for the whole process: call the function it returns in a finally / afterEach, and don’t use it in tests that run concurrently in the same process.

Telemetry listeners are process-wide, so always filter by source.pipelineName, and give each test pipeline its own name.

  • nextTelemetry(filter, signal?) resolves with the next matching event. Create the promise before the action, like an assertion before runAll().
  • onTelemetry(listener) receives every event and returns an unsubscribe function. Collect events, act, unsubscribe, assert.
import {
function test(name?: string, fn?: test.TestFn): Promise<void> (+3 overloads)

The test() function is the value imported from the test module. Each invocation of this function results in reporting the test to the TestsStream.

The TestContext object passed to the fn argument can be used to perform actions related to the current test. Examples include skipping the test, adding additional diagnostic information, or creating subtests.

test() returns a Promise that fulfills once the test completes. if test() is called within a suite, it fulfills immediately. The return value can usually be discarded for top level tests. However, the return value from subtests should be used to prevent the parent test from finishing first and cancelling the subtest as shown in the following example.

test('top level test', async (t) => {
// The setTimeout() in the following subtest would cause it to outlive its
// parent test if 'await' is removed on the next line. Once the parent test
// completes, it will cancel any outstanding subtests.
await t.test('longer running subtest', async (t) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
});
});

The timeout option can be used to fail the test if it takes longer than timeout milliseconds to complete. However, it is not a reliable mechanism for canceling tests because a running test might block the application thread and thus prevent the scheduled cancellation.

@since ― v18.0.0, v16.17.0

@param ― name The name of the test, which is displayed when reporting test results. Defaults to the name property of fn, or '<anonymous>' if fn does not have a name.

@param ― options Configuration options for the test.

@param ― fn The function under test. The first argument to this function is a TestContext object. If the test uses callbacks, the callback function is passed as the second argument.

@return ― Fulfilled with undefined once the test completes, or immediately if the test runs within a suite.

test
} from 'node:test';
import
function assert(value: unknown, message?: string | Error): asserts value
assert
from 'node:assert/strict';
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
,
function onTelemetry(listener: (event: TelemetryEvent) => void): () => void

Subscribe to every strategy event in the process. Works in every runtime (diagnostics_channel is Node-only). Returns an unsubscribe function.

onTelemetry
} from 'resilience-pipeline';
import {
class FakeTimeProvider

A clock you move by hand, so retries, timeouts and open circuits can be tested without waiting.

const time = new FakeTimeProvider();
const pipeline = new ResiliencePipelineBuilder({ timeProvider: time }).addRetry({ delay: '10s' }).addTimeout('1s').build();
// Attach the expectation first: the promise settles while runAll() fires timers.
const done = assert.rejects(pipeline.execute(() => neverResolves()), TimeoutRejectedError);
await time.runAll(); // 4 timeouts and 3 delays, instantly
await done;

FakeTimeProvider
} from 'resilience-pipeline/testing';
function test(name?: string, fn?: test.TestFn): Promise<void> (+3 overloads)

The test() function is the value imported from the test module. Each invocation of this function results in reporting the test to the TestsStream.

The TestContext object passed to the fn argument can be used to perform actions related to the current test. Examples include skipping the test, adding additional diagnostic information, or creating subtests.

test() returns a Promise that fulfills once the test completes. if test() is called within a suite, it fulfills immediately. The return value can usually be discarded for top level tests. However, the return value from subtests should be used to prevent the parent test from finishing first and cancelling the subtest as shown in the following example.

test('top level test', async (t) => {
// The setTimeout() in the following subtest would cause it to outlive its
// parent test if 'await' is removed on the next line. Once the parent test
// completes, it will cancel any outstanding subtests.
await t.test('longer running subtest', async (t) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
});
});

The timeout option can be used to fail the test if it takes longer than timeout milliseconds to complete. However, it is not a reliable mechanism for canceling tests because a running test might block the application thread and thus prevent the scheduled cancellation.

@since ― v18.0.0, v16.17.0

@param ― name The name of the test, which is displayed when reporting test results. Defaults to the name property of fn, or '<anonymous>' if fn does not have a name.

@param ― options Configuration options for the test.

@param ― fn The function under test. The first argument to this function is a TestContext object. If the test uses callbacks, the callback function is passed as the second argument.

@return ― Fulfilled with undefined once the test completes, or immediately if the test runs within a suite.

test
('emits one OnRetry per retry', async () => {
const
const time: FakeTimeProvider
time
= new
new FakeTimeProvider(start?: number | Date): FakeTimeProvider

A clock you move by hand, so retries, timeouts and open circuits can be tested without waiting.

const time = new FakeTimeProvider();
const pipeline = new ResiliencePipelineBuilder({ timeProvider: time }).addRetry({ delay: '10s' }).addTimeout('1s').build();
// Attach the expectation first: the promise settles while runAll() fires timers.
const done = assert.rejects(pipeline.execute(() => neverResolves()), TimeoutRejectedError);
await time.runAll(); // 4 timeouts and 3 delays, instantly
await done;

FakeTimeProvider
();
const
const events: string[]
events
: string[] = [];
const
const off: () => void
off
=
function onTelemetry(listener: (event: TelemetryEvent) => void): () => void

Subscribe to every strategy event in the process. Works in every runtime (diagnostics_channel is Node-only). Returns an unsubscribe function.

onTelemetry
((
e: TelemetryEvent
e
) => {
if (
e: TelemetryEvent
e
.
TelemetryEvent.source: TelemetrySource
source
.
TelemetrySource.pipelineName: string | undefined
pipelineName
=== 'inventory')
const events: string[]
events
.
Array<string>.push(...items: string[]): number

Appends new elements to the end of an array, and returns the new length of the array.

@param ― items New elements to add to the array.

push
(
e: TelemetryEvent
e
.
TelemetryEvent.event: string
event
);
});
try {
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
({
ResiliencePipelineBuilderOptions.name?: string | undefined

Labels telemetry events.

name
: 'inventory',
ResiliencePipelineBuilderOptions.timeProvider?: TimeProvider | undefined

Clock and timers for every strategy. Default: the system clock. Use FakeTimeProvider in tests.

timeProvider
:
const time: FakeTimeProvider
time
})
.
ResiliencePipelineBuilder<any, never, never>.addRetry(options?: RetryOptions<any>): ResiliencePipelineBuilder<any, never, never>
addRetry
({
RetryOptions<any>.delay?: Duration | undefined

Base delay. Default 2000 ms.

delay
: '1s',
RetryOptions<any>.maxRetryAttempts?: number | undefined

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

maxRetryAttempts
: 2 })
.
ResiliencePipelineBuilder<any, never, never>.build(): ResiliencePipeline<any, never>
build
();
let
let n: number
n
= 0;
const
const done: Promise<string>
done
=
const pipeline: ResiliencePipeline<any, never>
pipeline
.
ResiliencePipeline<any, never>.execute<string>(callback: (context: ResilienceContext) => MaybePromise<string>, options?: ExecuteOptions): Promise<string>

Runs the callback through the pipeline; throws the final error.

execute
(() => {
if (++
let n: number
n
< 3) throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('timeout talking to inventory');
return 'ok';
});
await
const time: FakeTimeProvider
time
.
FakeTimeProvider.runAll(limit?: number): Promise<void>

Fires timers until none are left (including ones scheduled while firing). Throws after limit timers to catch infinite loops.

runAll
();
await
const done: Promise<string>
done
;
} finally {
const off: () => void
off
();
}
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.deepEqual<string[]>(actual: unknown, expected: string[], message?: string | Error): asserts actual is string[]
export strict.deepEqual

Tests for deep equality between the actual and expected parameters. "Deep" equality means that the enumerable "own" properties of child objects are recursively evaluated also by the following rules.

@since ― v1.2.0

deepEqual
(
const events: string[]
events
, [
'OnPipelineExecuting',
'OnExecutionAttempt', 'OnRetry',
'OnExecutionAttempt', 'OnRetry',
'OnExecutionAttempt',
'OnStrategyExecuted', // one per strategy, outermost last
'OnPipelineExecuted',
]);
});

Event args carry the details: OnRetry has attemptNumber, retryDelay and duration; OnCircuitOpened has breakDuration; OnRateLimiterRejected has retryAfter. The full list is in the events reference.

Strategy order changes behaviour: a timeout added before a retry is a total budget, after it a per-attempt budget. pipeline.describe() lists the strategies outermost first, so a test can pin the order:

function test(name?: string, fn?: test.TestFn): Promise<void> (+3 overloads)

The test() function is the value imported from the test module. Each invocation of this function results in reporting the test to the TestsStream.

The TestContext object passed to the fn argument can be used to perform actions related to the current test. Examples include skipping the test, adding additional diagnostic information, or creating subtests.

test() returns a Promise that fulfills once the test completes. if test() is called within a suite, it fulfills immediately. The return value can usually be discarded for top level tests. However, the return value from subtests should be used to prevent the parent test from finishing first and cancelling the subtest as shown in the following example.

test('top level test', async (t) => {
// The setTimeout() in the following subtest would cause it to outlive its
// parent test if 'await' is removed on the next line. Once the parent test
// completes, it will cancel any outstanding subtests.
await t.test('longer running subtest', async (t) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
});
});

The timeout option can be used to fail the test if it takes longer than timeout milliseconds to complete. However, it is not a reliable mechanism for canceling tests because a running test might block the application thread and thus prevent the scheduled cancellation.

@since ― v18.0.0, v16.17.0

@param ― name The name of the test, which is displayed when reporting test results. Defaults to the name property of fn, or '<anonymous>' if fn does not have a name.

@param ― options Configuration options for the test.

@param ― fn The function under test. The first argument to this function is a TestContext object. If the test uses callbacks, the callback function is passed as the second argument.

@return ― Fulfilled with undefined once the test completes, or immediately if the test runs within a suite.

test
('payments: retry wraps the breaker and the per-attempt timeout', () => {
const
const steps: string[]
steps
=
function paymentsPipeline(timeProvider?: TimeProvider): ResiliencePipeline<Response, WithErrors<BrokenCircuitError, never, TimeoutRejectedError>>
paymentsPipeline
().
ResiliencePipeline<Response, WithErrors<BrokenCircuitError, never, TimeoutRejectedError>>.describe(): StrategyDescriptor[]

The strategies in execution order (outermost first), e.g. to assert a pipeline's configuration in tests.

describe
().
Array<StrategyDescriptor>.map<string>(callbackfn: (value: StrategyDescriptor, index: number, array: StrategyDescriptor[]) => string, thisArg?: any): string[]

Calls a defined callback function on each element of an array, and returns an array that contains the results.

@param ― callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.

@param ― thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.

map
(({
name: string | undefined
name
,
kind: string

Strategy class name, e.g. 'RetryStrategy'.

kind
}) => `${
name: string | undefined
name
}:${
kind: string

Strategy class name, e.g. 'RetryStrategy'.

kind
}`);
function assert(value: unknown, message?: string | Error): asserts value
assert
.
strict.deepEqual<string[]>(actual: unknown, expected: string[], message?: string | Error): asserts actual is string[]
export strict.deepEqual

Tests for deep equality between the actual and expected parameters. "Deep" equality means that the enumerable "own" properties of child objects are recursively evaluated also by the following rules.

@since ― v1.2.0

deepEqual
(
const steps: string[]
steps
, ['Retry:RetryStrategy', 'CircuitBreaker:CircuitBreakerStrategy', 'Timeout:TimeoutStrategy']);
});

name is the strategy’s name option (or its default, such as 'Retry'), kind is the class name, and strategy is the live instance.

  • Don’t combine with Jest or Vitest fake timers. FakeTimeProvider yields to the event loop with the real setTimeout(0). With vi.useFakeTimers() or jest.useFakeTimers() active, that yield never happens and runAll() hangs. You don’t need both: the pipeline only uses the clock you give it.
  • Your own timers are real. A callback that does await new Promise((r) => setTimeout(r, 100)) waits 100 real milliseconds; the fake clock doesn’t know about it. Simulate slowness with chaos latency or a promise you resolve yourself.
  • runAll() and background loops. circuitHealthCheck, sharedCircuitState and maxRetryAttempts: Infinity schedule timers forever, so runAll() throws after 10 000 timers. Use advance() for those.
  • Zero delays have no timer. delay: 0 resolves at once, so pendingTimers doesn’t count it.
  • Dispose stateful pipelines with background work (pipeline.dispose()) at the end of the test.