Skip to content

What counts as a failure

Every reactive strategy (retry, circuit breaker, hedging, fallback) asks one question about each outcome: is this a failure I should handle? The answer comes from its shouldHandle option. The defaults are deliberately narrow: handle transient failures, leave everything else alone.

Outcome Retry, circuit breaker, hedging Fallback
Errors: network errors, 5xx, timeouts, 408, 429 handled handled
Client errors: 4xx except 408/429 not handled not handled
Rejections: open circuit, full limiter, lock held not handled handled
Aborts (the caller went away) not handled not handled
Successful results, whatever their value not handled not handled

The reasoning behind each row:

  • Client errors. A 400 or 404 means the request is wrong, not that the dependency is down. Retrying won’t help, and counting it would let bad input open the breaker. 408 and 429 are the dependency pushing back, so they behave like 5xx.
  • Rejections. The call never started, so it says nothing about the dependency. A breaker that counted its own CIRCUIT_OPEN rejections would never close. Fallback does handle them: serving a cached value when the circuit is open is its main job.
  • Aborts. Nobody is waiting for the answer. A timeout is not a caller abort, though: a TimeoutRejectedError is handled like any other transient error.
  • Results. A resolved promise is a success unless you say otherwise with handleResult.
Check Rule Exported as
Client error A numeric status, statusCode or response.status in 400–499, except 408 and 429 isClientError(e)
Rejection Raised by a strategy that refused to start the call (CallRejectedError subclasses) isRejection(e)
Abort error.name === 'AbortError', or any error once the caller aborted the execution’s signal isAbortError(e) (name check only)

The status check is duck-typed, so it works with fetch wrappers, axios errors, and the AWS and Google SDKs. Throw your own error with a status property and the defaults apply:

class
class HttpError
HttpError
extends
var Error: ErrorConstructor
Error
{
constructor(readonly
HttpError.status: number
status
: number) {
super(`HTTP ${
status: number
status
}`);
}
}
await
retry<any>(fn: (context: ResilienceContext) => any, options?: RetryOptions<any> & CallOptions): Promise<any>

One-off retry, no pipeline needed: await retry(() => fetchUser(id), { maxRetryAttempts: 5, backoffType: 'exponential' }). Throw NonRetryableError from fn to stop early.

retry
(async ({
signal: AbortSignal

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

signal
}) => {
const
const res: Response
res
= await
function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+1 overload)
fetch
('https://api.example.com/items', {
RequestInit.signal?: AbortSignal | null | undefined

An AbortSignal to set request's signal.

signal
});
if (!
const res: Response
res
.
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
) throw new
constructor HttpError(status: number): HttpError
HttpError
(
const res: Response
res
.
Response.status: number

The status read-only property of the Response interface contains the HTTP status codes of the response.

MDN Reference

status
); // 503: retried. 404: thrown at once.
return
const res: Response
res
.
Body.json(): Promise<any>
json
();
});
  • Retry never retries an open circuit (CIRCUIT_OPEN / CIRCUIT_ISOLATED), whatever shouldHandle says.
  • A circuit breaker never counts rejections, and an error it doesn’t handle is neither a failure nor a success. Only a handled outcome counts as a failure; only an unhandled successful outcome counts as a success.

shouldHandle takes a function or a PredicateBuilder. It replaces the default, so your predicate decides alone about aborts and client errors too.

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

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

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

ResiliencePipelineBuilder
<
interface Response

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

MDN Reference

Response
>()
.
ResiliencePipelineBuilder<Response, never, never>.addRetry(options?: RetryOptions<Response>): ResiliencePipelineBuilder<Response, never, never>
addRetry
({
RetryOptions<Response>.shouldHandle?: ShouldHandle<RetryPredicateArgs<Response>> | undefined

Default: transient errors (not 4xx except 408/429, not aborts, not rejections). An open circuit is never retried.

shouldHandle
: new
new PredicateBuilder<Response>(): PredicateBuilder<Response>

Builds a shouldHandle predicate: new PredicateBuilder().handle(HttpError, e => e.status >= 500).handleResult(r => r.status === 429).

PredicateBuilder
<
interface Response

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

MDN Reference

Response
>()
.
PredicateBuilder<Response>.handle<Error>(type: ErrorClass<Error>, predicate?: (error: Error) => boolean): PredicateBuilder<Response>
handle
(
var TypeError: TypeErrorConstructor
TypeError
) // fetch network errors
.
PredicateBuilder<Response>.handle<DbError>(type: ErrorClass<DbError>, predicate?: (error: DbError) => boolean): PredicateBuilder<Response>
handle
(
class DbError
DbError
, (
e: DbError
e
) =>
e: DbError
e
.
DbError.code: string
code
=== '40001') // only this subtype
.
PredicateBuilder<Response>.handleInner<DbError>(type: ErrorClass<DbError>, predicate?: (error: DbError) => boolean): PredicateBuilder<Response>

Matches when the error, or any error in its cause chain, is a type (Polly's HandleInner).

handleInner
(
class DbError
DbError
) // or anywhere in the `cause` chain
.
PredicateBuilder<Response>.handleResult(predicate: (result: Response) => boolean): PredicateBuilder<Response>
handleResult
((
r: Response
r
) =>
r: Response
r
.
Response.status: number

The status read-only property of the Response interface contains the HTTP status codes of the response.

MDN Reference

status
>= 500 ||
r: Response
r
.
Response.status: number

The status read-only property of the Response interface contains the HTTP status codes of the response.

MDN Reference

status
=== 429),
})
.
ResiliencePipelineBuilder<Response, never, never>.build(): ResiliencePipeline<Response, never>
build
();
Method Matches
handle(Type, predicate?) The error is an instance of Type (and predicate returns true)
handleInner(Type, predicate?) The error or any error in its cause chain (10 levels deep) is a Type
handleResult(predicate) A successful result for which predicate returns true

The clauses are OR-ed. A function predicate receives { outcome, context } (retry adds attemptNumber) and may be async. To extend the default instead of replacing it, call it:

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

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

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

ResiliencePipelineBuilder
<
interface Response

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

MDN Reference

Response
>()
.
ResiliencePipelineBuilder<Response, never, never>.addRetry(options?: RetryOptions<Response>): ResiliencePipelineBuilder<Response, never, never>
addRetry
({
RetryOptions<Response>.shouldHandle?: ShouldHandle<RetryPredicateArgs<Response>> | undefined

Default: transient errors (not 4xx except 408/429, not aborts, not rejections). An open circuit is never retried.

shouldHandle
: (
args: RetryPredicateArgs<Response>
args
) =>
function defaultShouldHandle({ outcome, context }: OutcomeArgs<unknown>): boolean

Default for retry, circuit breaker and hedging: transient failures only. Not handled: results, aborts, client errors (4xx except 408/429), rejections from other strategies.

defaultShouldHandle
(
args: RetryPredicateArgs<Response>
args
) || (
args: RetryPredicateArgs<Response>
args
.
OutcomeArgs<Response>.outcome: Outcome<Response, unknown>
outcome
.
ok: boolean
ok
&&
args: RetryPredicateArgs<Response>
args
.
OutcomeArgs<Response>.outcome: {
readonly ok: true;
readonly result: Response;
}
outcome
.
result: Response
result
.
Response.status: number

The status read-only property of the Response interface contains the HTTP status codes of the response.

MDN Reference

status
>= 500),
})
.
ResiliencePipelineBuilder<Response, never, never>.build(): ResiliencePipeline<Response, never>
build
();

defaultFallbackShouldHandle is the fallback’s default, exported the same way.

When the last attempt still produces a handled result (say, a 503 Response), retry returns that result. It doesn’t throw. Check the status after execute(), or throw inside the callback as in the HttpError example above.

Throw NonRetryableError from the callback to stop retrying immediately. The pipeline unwraps it, so the caller receives the original error:

await
retry<{
declined: boolean;
reason: string;
}>(fn: (context: ResilienceContext) => MaybePromise<{
declined: boolean;
reason: string;
}>, options?: RetryOptions<{
declined: boolean;
reason: string;
}> & CallOptions): Promise<{
declined: boolean;
reason: string;
}>

One-off retry, no pipeline needed: await retry(() => fetchUser(id), { maxRetryAttempts: 5, backoffType: 'exponential' }). Throw NonRetryableError from fn to stop early.

retry
(async () => {
const
const result: {
declined: boolean;
reason: string;
}
result
= await
function charge(): Promise<{
declined: boolean;
reason: string;
}>
charge
();
if (
const result: {
declined: boolean;
reason: string;
}
result
.
declined: boolean
declined
) throw new
new NonRetryableError(errorOrMessage: unknown): NonRetryableError

Throw (or wrap an error in) this to stop retrying right away: retry gives up and the caller receives the original error. Like p-retry's AbortError. throw new NonRetryableError(error) or throw new NonRetryableError('invalid card').

NonRetryableError
(
const result: {
declined: boolean;
reason: string;
}
result
.
reason: string
reason
); // the caller gets Error(reason)
return
const result: {
declined: boolean;
reason: string;
}
result
;
});