Skip to content

Commit 96cce98

Browse files
committed
Add retryDelay to onFailedAttempt context
Fixes #66
1 parent 0b1e298 commit 96cce98

4 files changed

Lines changed: 58 additions & 11 deletions

File tree

index.d.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ export type RetryContext = {
1515
readonly attemptNumber: number;
1616
readonly retriesLeft: number;
1717
readonly retriesConsumed: number;
18+
19+
/**
20+
The delay in milliseconds before the next retry attempt.
21+
22+
This is calculated based on `minTimeout`, `factor`, `maxTimeout`, and `randomize` options.
23+
24+
Note: The actual delay may be shorter if it would exceed `maxRetryTime`.
25+
*/
26+
readonly retryDelay: number;
1827
};
1928

2029
export type Options = {
@@ -40,10 +49,10 @@ export type Options = {
4049
};
4150
4251
const result = await pRetry(run, {
43-
onFailedAttempt: ({error, attemptNumber, retriesLeft, retriesConsumed}) => {
44-
console.log(`Attempt ${attemptNumber} failed. ${retriesLeft} retries left. ${retriesConsumed} retries consumed.`);
45-
// 1st request => Attempt 1 failed. 5 retries left. 0 retries consumed.
46-
// 2nd request => Attempt 2 failed. 4 retries left. 1 retries consumed.
52+
onFailedAttempt: ({error, attemptNumber, retriesLeft, retriesConsumed, retryDelay}) => {
53+
console.log(`Attempt ${attemptNumber} failed. Retrying in ${retryDelay}ms. ${retriesLeft} retries left.`);
54+
// 1st request => Attempt 1 failed. Retrying in 1000ms. 5 retries left.
55+
// 2nd request => Attempt 2 failed. Retrying in 2000ms. 4 retries left.
4756
// …
4857
},
4958
retries: 5

index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,15 @@ async function onAttemptFailure({error, attemptNumber, retriesConsumed, startTim
8282

8383
const maxRetryTime = options.maxRetryTime ?? Number.POSITIVE_INFINITY;
8484

85+
// Calculate the delay upfront so it can be included in the context
86+
const delayTime = calculateDelay(retriesConsumed, options);
87+
8588
const context = Object.freeze({
8689
error: normalizedError,
8790
attemptNumber,
8891
retriesLeft,
8992
retriesConsumed,
93+
retryDelay: delayTime,
9094
});
9195

9296
await options.onFailedAttempt(context);
@@ -121,7 +125,6 @@ async function onAttemptFailure({error, attemptNumber, retriesConsumed, startTim
121125
return false;
122126
}
123127

124-
const delayTime = calculateDelay(retriesConsumed, options);
125128
const finalDelay = Math.min(delayTime, remainingTime);
126129

127130
options.signal?.throwIfAborted();

readme.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,10 @@ const run = async () => {
7171
};
7272

7373
const result = await pRetry(run, {
74-
onFailedAttempt: ({error, attemptNumber, retriesLeft, retriesConsumed}) => {
75-
console.log(`Attempt ${attemptNumber} failed. ${retriesLeft} retries left. ${retriesConsumed} retries consumed.`);
76-
// 1st request => Attempt 1 failed. 5 retries left. 0 retries consumed.
77-
// 2nd request => Attempt 2 failed. 4 retries left. 1 retries consumed.
74+
onFailedAttempt: ({error, attemptNumber, retriesLeft, retriesConsumed, retryDelay}) => {
75+
console.log(`Attempt ${attemptNumber} failed. Retrying in ${retryDelay}ms. ${retriesLeft} retries left.`);
76+
// 1st request => Attempt 1 failed. Retrying in 1000ms. 5 retries left.
77+
// 2nd request => Attempt 2 failed. Retrying in 2000ms. 4 retries left.
7878
//
7979
},
8080
retries: 5
@@ -83,6 +83,13 @@ const result = await pRetry(run, {
8383
console.log(result);
8484
```
8585

86+
The `context` object contains:
87+
- `error` - The error that was thrown
88+
- `attemptNumber` - The attempt number (starts at 1)
89+
- `retriesLeft` - Number of retries remaining
90+
- `retriesConsumed` - Number of retries consumed so far
91+
- `retryDelay` - The delay in milliseconds before the next retry (based on `minTimeout`, `factor`, `maxTimeout`, and `randomize`)
92+
8693
The `onFailedAttempt` function can return a promise. For example, to add a [delay](https://github.com/sindresorhus/delay):
8794

8895
```js
@@ -244,7 +251,7 @@ const response = await fetchWithRetry('https://sindresorhus.com/unicorn');
244251
### AbortError(message)
245252
### AbortError(error)
246253

247-
Abort retrying and reject the promise. No callbacks functions will be called.
254+
Abort retrying and reject the promise. No callback functions will be called.
248255

249256
### message
250257

test.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import process from 'node:process';
22
import fs from 'node:fs/promises';
33
import path from 'node:path';
4-
import {performance} from 'node:perf_hooks';
54
import {execa} from 'execa';
65
import test from 'ava';
76
import delay from 'delay';
@@ -1267,6 +1266,35 @@ test('shouldConsumeRetry returning false still calls shouldRetry', async t => {
12671266
t.is(shouldRetryCalls, 1);
12681267
});
12691268

1269+
test.serial('retryDelay is provided in onFailedAttempt context', async t => {
1270+
const retryDelays = [];
1271+
const originalSetTimeout = setTimeout;
1272+
1273+
globalThis.setTimeout = (function_, _ms) => originalSetTimeout(function_, 0);
1274+
1275+
t.teardown(() => {
1276+
globalThis.setTimeout = originalSetTimeout;
1277+
});
1278+
1279+
await t.throwsAsync(pRetry(
1280+
async () => {
1281+
throw new Error('test');
1282+
},
1283+
{
1284+
retries: 3,
1285+
factor: 2,
1286+
minTimeout: 100,
1287+
randomize: false,
1288+
onFailedAttempt({retryDelay}) {
1289+
retryDelays.push(retryDelay);
1290+
},
1291+
},
1292+
));
1293+
1294+
// Verify retryDelay follows exponential backoff formula
1295+
t.deepEqual(retryDelays, [100, 200, 400, 800]);
1296+
});
1297+
12701298
test.serial('Only consumed retries advance backoff', async t => {
12711299
let attempts = 0;
12721300
const timeouts = [];

0 commit comments

Comments
 (0)