Skip to content

Batches and waitFor

Promise.all has two problems in resilient code: when one task fails, the others keep running and burn quota for a result nobody will read, and it has no concurrency cap. These helpers fix both. Each task runs through an optional pipeline on its own (its own retries and timeout), receives a context with a signal, and is cancelled when the batch gives up. waitFor covers the other common loop: poll until something is ready.

They are plain functions, not pipeline strategies. To merge many calls into one request (DataLoader-style), use the batcher instead.

Runs tasks in parallel. On the first failure, the others are aborted through their context signal, and all rejects with that failure. The result is a typed tuple.

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

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

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

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

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

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

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

addTimeout
('2s').
ResiliencePipelineBuilder<any, TimeoutRejectedError, never>.build(): ResiliencePipeline<any, TimeoutRejectedError>
build
();
const [
const user: User
user
,
const orders: Order[]
orders
] = await
all<[User, Order[]]>(tasks: readonly [(context: ResilienceContext) => MaybePromise<User>, (context: ResilienceContext) => MaybePromise<Order[]>], options?: BatchOptions): Promise<[User, Order[]]>

Runs the tasks in parallel. On the first failure the others are aborted (through their context signal) and it rejects.

all
(
[({
signal: AbortSignal

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

signal
}) =>
function getUser(id: string, signal: AbortSignal): Promise<User>
getUser
('42',
signal: AbortSignal

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

signal
), ({
signal: AbortSignal

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

signal
}) =>
function getOrders(id: string, signal: AbortSignal): Promise<Order[]>
getOrders
('42',
signal: AbortSignal

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

signal
)],
{
BatchOptions.pipeline?: ResiliencePipeline<any, any> | undefined

Each task / item runs through it separately (e.g. its own retries and timeout).

pipeline
,
BatchOptions.signal?: AbortSignal | undefined
signal
:
var AbortSignal: {
new (): AbortSignal;
prototype: AbortSignal;
abort(reason?: any): AbortSignal;
any(signals: AbortSignal[]): AbortSignal;
timeout(milliseconds: number): AbortSignal;
}

The AbortSignal interface represents a signal object that allows you to communicate with an asynchronous operation (such as a fetch request) and abort it if required via an AbortController object.

MDN Reference

AbortSignal
.
function timeout(milliseconds: number): AbortSignal

The AbortSignal.timeout() static method returns an AbortSignal that will automatically abort after a specified time.

MDN Reference

timeout
(5000) },
);

Runs every task to the end and returns one Outcome per task, in order. It never rejects, except when the caller’s signal aborts (it then rejects with the abort reason once the tasks have settled).

const
const outcomes: [Outcome<number>, Outcome<number>]
outcomes
= await
allSettled<[number, number]>(tasks: readonly [(context: ResilienceContext) => MaybePromise<number>, (context: ResilienceContext) => MaybePromise<number>], options?: BatchOptions): Promise<[Outcome<number>, Outcome<number>]>

Runs every task (through the optional pipeline) and returns one Outcome per task, in order; never rejects except on caller abort.

allSettled
([({
signal: AbortSignal

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

signal
}) =>
function ping(host: string, signal: AbortSignal): Promise<number>
ping
('eu',
signal: AbortSignal

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

signal
), ({
signal: AbortSignal

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

signal
}) =>
function ping(host: string, signal: AbortSignal): Promise<number>
ping
('us',
signal: AbortSignal

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

signal
)]);
for (const
const o: Outcome<number>
o
of
const outcomes: [Outcome<number>, Outcome<number>]
outcomes
)
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@see ― source

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@since ― v0.1.100

log
(
const o: Outcome<number>
o
.
ok: boolean
ok
? `${
const o: {
readonly ok: true;
readonly result: number;
}
o
.
result: number
result
} ms` : `down: ${
var String: StringConstructor
(value?: any) => string

Allows manipulation and formatting of text strings and determination and location of substrings within strings.

String
(
const o: {
readonly ok: false;
readonly error: unknown;
}
o
.
error: unknown
error
)}`);

Maps items with at most concurrency running at once. Results come back in input order.

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
().
ResiliencePipelineBuilder<any, never, never>.addTimeout(options: Duration | TimeoutOptions): Next<any, never, never, TimeoutRejectedError>

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

addTimeout
('10s').
ResiliencePipelineBuilder<any, TimeoutRejectedError, never>.build(): ResiliencePipeline<any, TimeoutRejectedError>
build
();
// Stops at the first failure and aborts the items still running.
const
const thumbs: string[]
thumbs
= await
map<string, string>(items: readonly string[], fn: (item: string, index: number, context: ResilienceContext) => MaybePromise<string>, options?: MapOptions): Promise<string[]> (+1 overload)

Maps items with at most concurrency running at once; results come back in input order.

map
(
const urls: string[]
urls
, (
url: string
url
,
_i: number
_i
, {
signal: AbortSignal

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

signal
}) =>
function resize(url: string, signal: AbortSignal): Promise<string>
resize
(
url: string
url
,
signal: AbortSignal

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

signal
), {
MapOptions.concurrency?: number | undefined

Max items running at once. Default: all.

concurrency
: 8,
BatchOptions.pipeline?: ResiliencePipeline<any, any> | undefined

Each task / item runs through it separately (e.g. its own retries and timeout).

pipeline
});
// Runs every item and reports each one.
const
const results: Outcome<string>[]
results
= await
map<string, string>(items: readonly string[], fn: (item: string, index: number, context: ResilienceContext) => MaybePromise<string>, options: MapOptions & {
stopOnError: false;
}): Promise<Outcome<string>[]> (+1 overload)

Maps items with at most concurrency running at once; results come back in input order.

map
(
const urls: string[]
urls
, (
url: string
url
,
_i: number
_i
, {
signal: AbortSignal

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

signal
}) =>
function resize(url: string, signal: AbortSignal): Promise<string>
resize
(
url: string
url
,
signal: AbortSignal

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

signal
), {
MapOptions.concurrency?: number | undefined

Max items running at once. Default: all.

concurrency
: 8,
stopOnError: false

true (default): reject on the first failure and abort the rest. false: run every item and return an Outcome per item.

stopOnError
: false });
const
const failed: number
failed
=
const results: Outcome<string>[]
results
.
Array<Outcome<string>>.filter<{
readonly ok: false;
readonly error: unknown;
}>(predicate: (value: Outcome<string>, index: number, array: Outcome<string>[]) => value is {
readonly ok: false;
readonly error: unknown;
}, thisArg?: any): {
readonly ok: false;
readonly error: unknown;
}[] (+1 overload)

Returns the elements of an array that meet the condition specified in a callback function.

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

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

filter
((
r: Outcome<string>
r
) => !
r: Outcome<string>
r
.
ok: boolean
ok
).
Array<{ readonly ok: false; readonly error: unknown; }>.length: number

Gets or sets the length of the array. This is a number one higher than the highest index in the array.

length
;

With stopOnError: false the return type is Outcome<R>[]; otherwise R[].

all, allSettled and map accept:

Option Type Default Description
pipeline ResiliencePipeline the empty pipeline Each task or item runs through it separately (its own retries, timeout, breaker).
signal AbortSignal – Cancels the batch: running tasks see their signal abort, unstarted items never start, and the call rejects with the abort reason.

map adds:

Option Type Default Description
concurrency number all items at once Max items running at once. ≥ 1.
stopOnError boolean true true: reject on the first failure and abort the rest. false: run every item and return an Outcome per item.

The callback gets (item, index, context). Pass context.signal to your I/O, or cancellation can’t reach it.

Polls condition until it returns true. Rejects with TimeoutRejectedError after timeout, or with the error if condition throws.

// Fixed interval.
await
function waitFor(condition: (signal: AbortSignal) => MaybePromise<boolean>, options: WaitForOptions): Promise<void>

Polls condition every interval until it returns true. Rejects with TimeoutRejectedError after timeout, and with the error if condition throws (wrap it in .catch(() => false) to treat errors as "not yet").

waitFor
(async (
signal: AbortSignal
signal
) => (await
function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+1 overload)
fetch
('http://localhost:8080/health', {
RequestInit.signal?: AbortSignal | null | undefined

An AbortSignal to set request's signal.

signal
})).
Response.ok: boolean

The ok read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not.

MDN Reference

ok
, {
WaitForOptions.interval: Duration | ((attempt: number) => Duration)

Pause between checks: fixed, or per poll (e.g. backoff: (n) => Math.min(100 * 2 ** n, 5000)).

interval
: '500ms',
WaitForOptions.timeout: Duration

Rejects with TimeoutRejectedError after this long.

timeout
: '30s',
});
// Backoff: 200 ms, 400 ms, 800 ms, ... capped at 5 s.
await
function waitFor(condition: (signal: AbortSignal) => MaybePromise<boolean>, options: WaitForOptions): Promise<void>

Polls condition every interval until it returns true. Rejects with TimeoutRejectedError after timeout, and with the error if condition throws (wrap it in .catch(() => false) to treat errors as "not yet").

waitFor
(
async (
signal: AbortSignal
signal
) => {
const
const job: any
job
= await
function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+1 overload)
fetch
(`https://api.example.com/jobs/${
const jobId: string
jobId
}`, {
RequestInit.signal?: AbortSignal | null | undefined

An AbortSignal to set request's signal.

signal
}).
Promise<Response>.then<any, never>(onfulfilled?: ((value: Response) => any) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<any>

Attaches callbacks for the resolution and/or rejection of the Promise.

@param ― onfulfilled The callback to execute when the Promise is resolved.

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

@returns ― A Promise for the completion of which ever callback is executed.

then
((
r: Response
r
) =>
r: Response
r
.
Body.json(): Promise<any>
json
());
return
const job: any
job
.
any
status
=== 'done';
},
{
WaitForOptions.interval: Duration | ((attempt: number) => Duration)

Pause between checks: fixed, or per poll (e.g. backoff: (n) => Math.min(100 * 2 ** n, 5000)).

interval
: (
n: number
n
) =>
var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.
Math.min(...values: number[]): number

Returns the smaller of a set of supplied numeric expressions.

@param ― values Numeric expressions to be evaluated.

min
(100 * 2 **
n: number
n
, 5000),
WaitForOptions.timeout: Duration

Rejects with TimeoutRejectedError after this long.

timeout
: '10m' },
);

The first check runs immediately. Between checks it pauses interval, or interval(n) where n is 1 before the second check, 2 before the third, and so on. The backoff helpers produce such functions (interval: backoff.exponential({ delay: '100ms', maxDelay: '5s' })), but they count from 0, so the first pause is their second step: 200 ms here, and backoff.sequence(['100ms', '1s']) starts at 1 s.

Option Type Default Description
interval Duration | (attempt: number) => Duration required Pause between checks: fixed, or per pause. ≥ 0.
timeout Duration required Give up after this long with TimeoutRejectedError. > 0.
signal AbortSignal – Cancels waiting; rejects with its reason.
timeProvider TimeProvider system clock Clock for the interval and timeout, e.g. FakeTimeProvider in tests.

condition gets a signal that aborts on timeout or cancellation. If it ignores the signal, waitFor still stops waiting for it at the deadline. A throwing condition ends the wait; to treat errors as “not yet”, catch them: (signal) => check(signal).catch(() => false).