Retry utility function supporting exponential backoff powered by RxJS.
$ yarn add @geersch/retry
Declare a function that should be retried in case an error occured.
async function operation(): Promise<string> {
return 'Success!';
}
// OR
function operation(): string {
return 'Success!;
}
Invoke it with the retry
utility function and specify a backoff strategy.
const result = await retry((attempt: number) => {
operation();
}, EqualJitterBackoffStrategy);
If an error occurs during the operation()
, it will be retried using the EqualJitterBackoffStrategy
.
You can pass the type of backoff strategy to use, in which case it will be instantiated for you with its default base delay, or you can instantiate it yourself and override the base delay as needed.
const result = await retry((attempt: number) => {
operation();
}, new EqualJitterBackoffStrategy({ baseDelay: 200 }));
The current attempt is passed as a parameter to the retried function. The maximum number of retries can be passed to the backoff strategy. If the maximum is set to 5, then the function will be executed a maximum of 6 times (1 initial call + 5 retries).
The following backoff strategies are supported out of the box:
- DecorrelatedJitterBackoffStrategy
- EqualJitterBackoffStrategy
- ExponentialBackoffStrategy
- FibonacciBackoffStrategy
- FixedBackoffStrategyConfig
- FullJitterBackOffStrategy
- LinearBackoffStrategy
Each of these strategies has a default base delay of 100
ms, which is the initial delay time that sets the baseline for calculating subsequent delays during retries.
Similar to the full jitter backoff strategy, but it increases the maximum jitter based on the last calculated delay.
For example, with a base delay of 100
ms:
- A final delay between
0
and ±137.72
ms is calculated for retry1
. - A final delay between
100
and ±146.01
ms is calculated for retry2
. - A final delay between
100
and ±286.58
ms is calculated for retry3
. - ...
By default, the DecorrelatedJitterBackoffStrategy
uses a base delay of 100
ms.
const result = await retry((attempt: number) => {
operation();
}, DecorrelatedJitterBackoffStrategy);
The default base delay of 100
ms can be overridden.
const result = await retry((attempt: number) => {
operation();
}, new DecorrelatedJitterBackoffStrategy({ baseDelay: 200 }));
Uses an equal jitter for computing the delay before the next retry. First, an exponential delay is calculated based on the current retry attempt and configured base delay.
For example, with a base delay of 100
ms:
- An exponential delay of
200
ms is calculated for retry1
. - An exponential delay of
400
ms is calculated for retry2
. - An exponential delay of
800
ms is calculated for retry3
. - ...
Half of this exponential delay is kept, and a random value (jitter) between 0 and half of the exponential delay plus one is added.
- A final delay between
100
and201
ms is calculated for retry1
. - A final delay between
200
and401
ms is calculated for retry2
. - A final delay between
400
and801
ms is calculated for retry3
. - ...
By default the EqualJitterBackoffStrategy
uses a base delay of 100
ms.
const result = await retry((attempt: number) => {
operation();
}, EqualJitterBackoffStrategy);
The default base delay of 100
ms can be overridden.
const result = await retry((attempt: number) => {
operation();
}, new EqualJitterBackoffStrategy({ baseDelay: 200 }));
Calculates an exponential delay based on the current retry attempt and configured base delay.
For example, with a base delay of 100
ms:
- An exponential delay of
200
ms is calculated for retry1
. - An exponential delay of
400
ms is calculated for retry2
. - An exponential delay of
800
ms is calculated for retry3
. - ...
By default, the ExponentialBackoffStrategy
uses a base delay of 100
ms.
const result = await retry((attempt: number) => {
operation();
}, ExponentialBackoffStrategy);
The default base delay of 100
ms can be overridden.
const result = await retry((attempt: number) => {
operation();
}, new ExponentialBackoffStrategy({ baseDelay: 200 }));
The FibonacciBackOffStrategy
computes the delay before the next retry using a Fibonacci sequence approach. It calculates the delay based on the current retry attempt and a configured base delay.
For example, with a base delay of 100
ms:
- A delay of
100
ms is calculated for retry1
. - A delay of
100
ms is calculated for retry2
. - A delay of
200
ms is calculated for retry3
. - A delay of
300
ms is calculated for retry4
. - A delay of
500
ms is calculated for retry5
. - ...
By default, the FibonacciBackOffStrategy
uses a base delay of 100
ms.
const result = await retry(async (attempt: number) => {
await operation();
}, FibonacciBackOffStrategy);
The default base delay of 100
ms can be overridden.
const result = await retry(async (attempt: number) => {
await operation();
}, new FibonacciBackOffStrategy({ baseDelay: 200 }));
Uses the same delay between every retry attempt. By default, the FixedBackoffStrategyConfig
uses a base delay of 100
ms. As the name suggests, the delay is fixed. The configured base delay will be used for all attempts.
const result = await retry((attempt: number) => {
operation();
}, FixedBackoffStrategyConfig);
The default base delay of 100
ms can be overridden.
const result = await retry((attempt: number) => {
operation();
}, new FixedBackoffStrategyConfig({ baseDelay: 200 }));
It uses full jitter for computing the delay before the next retry. First, an exponential delay is calculated based on the current retry attempt and the configured base delay.
For example, with a base delay of 100
ms:
- An exponential delay of
200
ms is calculated for retry1
. - An exponential delay of
400
ms is calculated for retry2
. - An exponential delay of
800
ms is calculated for retry3
. - ...
Then, a random delay between 0
and this exponential delay is taken.
- A final delay between
0
and200
ms is calculated for retry1
. - A final delay between
0
and400
ms is calculated for retry2
. - A final delay between
0
and800
ms is calculated for retry3
. - ...
By default, the FullJitterBackOffStrategy
uses a base delay of 100 ms.
const result = await retry((attempt: number) => {
operation();
}, FullJitterBackOffStrategy);
The default base delay of 100
ms can be overridden.
const result = await retry((attempt: number) => {
operation();
}, new FullJitterBackOffStrategy({ baseDelay: 200 }));
The base delay is multiplied by the current retry attempt to calculate the delay before the next retry.
For example, with a base delay of 100
ms:
- A delay of
100
ms is calculated for retry1
. - A delay of
200
ms is calculated for retry2
. - A delay of
400
ms is calculated for retry3
. - ...
const result = await retry((attempt: number) => {
operation();
}, LinearBackoffStrategy);
The default base delay of 100
ms can be overridden.
const result = await retry((attempt: number) => {
operation();
}, new LinearBackoffStrategy({ baseDelay: 200 }));
In addition to a backoff strategy, you can also pass optional retry options as a third parameter.
const result = await retry(
(attempt: number) => {
operation();
},
EqualJitterBackoffStrategy,
{
abortRetry: (err: any, retryCount: number) => true,
scaleFactor: 2,
unrecoverableErrors: [RecoverableError, UnrecoverableError],
},
);
A synchronous callback function that is executed before every retry attempt. It is passed the error that occurred and the current retry attempt. Return true
if you want to abort, or false
to keep trying.
const result = await retry(
(attempt: number) => {
operation();
},
EqualJitterBackoffStrategy,
{
abortRetry: (err: any, retryCount: number) => {
if (err instanceof TypeError) {
return true;
}
return false;
},
},
);
To clamp the delay calculated by the backoff strategy, use the maxDelay option. It defaults to 30000
milliseconds.
The following example clamps the delay calculated by the backoff strategy to a maximum of 10000
milliseconds.
const result = await retry(
(attempt: number) => {
operation();
},
EqualJitterBackoffStrategy,
{
maxDelay: 10000,
},
);
Use the maxRetries
option to specify the maximum number of retry attempts. It defaults to 5
.
The following example will execute the operation()
a maximum of 4
times - once for the initial invocation, plus a maximum of 3
retries.
const result = await retry(
(attempt: number) => {
operation();
},
EqualJitterBackoffStrategy,
{
maxRetries: 3,
},
);
Certain strategies can quickly lead to long delays between retry attempts. To offset this, while still retaining the curve between retry attempts, you can specify a scaleFactor
. It can be used to reduce or increase the calculated delay.
For example, a scaleFactor
of 0.5
will halve the computed delay, but a scaleFactor
of 2
will double it.
const result = await retry(
(attempt: number) => {
operation();
},
EqualJitterBackoffStrategy,
{
scaleFactor: 0.5,
},
);
Similar to the abortRetry
option, but instead of a callback function, you can specify an array of error types. If the error that occurred is an instance of one of the specified error types, the retry attempts will be aborted.
class UnrecoverableError extends Error {}
async function operation(): Promise<void> {
throw new UnrecoverableError();
}
const result = await retry(
(attempt: number) => {
operation();
},
EqualJitterBackoffStrategy,
{
unrecoverableErrors: [UnrecoverableError],
},
);
To create a custom backoff strategy implement the BackoffStrategy
interface.
export interface BackoffStrategy {
getGenerator(maxRetries: number): Generator<number>;
}
Each backoff strategy is essentially a generator that returns the next delay.
Here's an example of implementing a backoff strategy that calculates the next delay based on the Fibonacci sequence. Based on the current retry attempt, the corresponding Fibonacci number is calculated and used as the delay before the next retry.
export class FibonacciBackoffStrategy implements BackoffStrategy {
private readonly baseDelay: number;
private prevDelay: number;
private currentDelay: number;
constructor({ baseDelay = 100 }: BackoffStrategyConfig = {}) {
this.baseDelay = baseDelay;
this.prevDelay = 0;
this.currentDelay = 1;
}
*getGenerator(maxRetries: number): Generator<number> {
let attempt = 0;
while (attempt < maxRetries) {
const nextDelay = this.baseDelay * this.currentDelay;
yield nextDelay;
const sum = this.prevDelay + this.currentDelay;
this.prevDelay = this.currentDelay;
this.currentDelay = sum;
attempt += 1;
}
}
}
The maximum number of retries is passed to the getGenerator()
function, allowing you to determine when to yield the last delay. If the generator yields fewer delays than the number of maximum retries, an error will be thrown. You can also create a generator that will keep yielding delays.
This package is MIT licensed.