Skip to content

Commit bc2b3b7

Browse files
dayongkrraon0211autofix-ci[bot]
authored
feat(retry): add shouldRetry option (#1585)
* feat(retry): add shouldRetry option Add shouldRetry option to control retry behavior based on error type. This allows users to retry only on specific errors (e.g., 500+ status codes). * Apply suggestions from code review * [autofix.ci] apply automated fixes * fix --------- Co-authored-by: Sojin Park <raon0211@toss.im> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 5fa7974 commit bc2b3b7

File tree

6 files changed

+177
-0
lines changed

6 files changed

+177
-0
lines changed

docs/ja/reference/function/retry.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,33 @@ const data4 = await retry(
5252
);
5353
```
5454

55+
特定のエラーでのみ再試行したい場合は `shouldRetry` オプションを使用できます。
56+
57+
```typescript
58+
import { retry } from 'es-toolkit/function';
59+
60+
class NetworkError extends Error {
61+
constructor(public status: number) {
62+
super(`Network error: ${status}`);
63+
}
64+
}
65+
66+
// 500エラー以上でのみ再試行
67+
const data5 = await retry(
68+
async () => {
69+
const response = await fetch('/api/data');
70+
if (!response.ok) {
71+
throw new NetworkError(response.status);
72+
}
73+
return response.json();
74+
},
75+
{
76+
retries: 3,
77+
shouldRetry: (error, attempt) => error instanceof NetworkError && error.status >= 500,
78+
}
79+
);
80+
```
81+
5582
AbortSignal を使用して再試行をキャンセルすることもできます。
5683

5784
```typescript
@@ -86,6 +113,9 @@ try {
86113
- `retries` (`number`, オプション): 再試行する回数です。デフォルトは `Infinity` で無限に再試行します。
87114
- `delay` (`number | (attempts: number) => number`, オプション): 再試行間隔(ミリ秒)です。数値または関数を使用できます。デフォルトは `0` です。
88115
- `signal` (`AbortSignal`, オプション): 再試行をキャンセルできるシグナルです。
116+
- `shouldRetry` (`(error: unknown, attempt: number) => boolean`, オプション): 再試行するかどうかを決定する関数です。`false` を返すと即座にエラーをスローします。
117+
- `error`: 発生したエラーオブジェクトです。
118+
- `attempt`: 現在の試行回数です (0から開始)。
89119

90120
#### 戻り値
91121

docs/ko/reference/function/retry.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,33 @@ const data4 = await retry(
5252
);
5353
```
5454

55+
특정 에러에서만 재시도하고 싶을 때 `shouldRetry` 옵션을 사용할 수 있어요.
56+
57+
```typescript
58+
import { retry } from 'es-toolkit/function';
59+
60+
class NetworkError extends Error {
61+
constructor(public status: number) {
62+
super(`Network error: ${status}`);
63+
}
64+
}
65+
66+
// 500 에러 이상에서만 재시도
67+
const data5 = await retry(
68+
async () => {
69+
const response = await fetch('/api/data');
70+
if (!response.ok) {
71+
throw new NetworkError(response.status);
72+
}
73+
return response.json();
74+
},
75+
{
76+
retries: 3,
77+
shouldRetry: (error, attempt) => error instanceof NetworkError && error.status >= 500,
78+
}
79+
);
80+
```
81+
5582
AbortSignal을 사용해서 재시도를 취소할 수도 있어요.
5683

5784
```typescript
@@ -86,6 +113,9 @@ try {
86113
- `retries` (`number`, 선택): 재시도할 횟수예요. 기본값은 `Infinity`로 무한 재시도해요.
87114
- `delay` (`number | (attempts: number) => number`, 선택): 재시도 간격(밀리초)이에요. 숫자나 함수를 사용할 수 있어요. 기본값은 `0`이에요.
88115
- `signal` (`AbortSignal`, 선택): 재시도를 취소할 수 있는 시그널이에요.
116+
- `shouldRetry` (`(error: unknown, attempt: number) => boolean`, 선택): 재시도 여부를 결정하는 함수예요. `false`를 반환하면 즉시 에러를 던져요.
117+
- `error`: 발생한 에러 객체예요.
118+
- `attempt`: 현재 시도 횟수예요 (0부터 시작).
89119

90120
#### 반환 값
91121

docs/reference/function/retry.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,33 @@ const data4 = await retry(
5252
);
5353
```
5454

55+
You can use `shouldRetry` option when you want to retry only on specific errors.
56+
57+
```typescript
58+
import { retry } from 'es-toolkit/function';
59+
60+
class NetworkError extends Error {
61+
constructor(public status: number) {
62+
super(`Network error: ${status}`);
63+
}
64+
}
65+
66+
// Retry only on 500+ errors
67+
const data5 = await retry(
68+
async () => {
69+
const response = await fetch('/api/data');
70+
if (!response.ok) {
71+
throw new NetworkError(response.status);
72+
}
73+
return response.json();
74+
},
75+
{
76+
retries: 3,
77+
shouldRetry: (error, attempt) => error instanceof NetworkError && error.status >= 500,
78+
}
79+
);
80+
```
81+
5582
You can also cancel retries using AbortSignal.
5683

5784
```typescript
@@ -86,6 +113,9 @@ try {
86113
- `retries` (`number`, optional): The number of times to retry. Defaults to `Infinity` for infinite retries.
87114
- `delay` (`number | (attempts: number) => number`, optional): The retry interval (in milliseconds). Can be a number or a function. Defaults to `0`.
88115
- `signal` (`AbortSignal`, optional): A signal that can cancel retries.
116+
- `shouldRetry` (`(error: unknown, attempt: number) => boolean`, optional): A function that determines whether to retry. If it returns `false`, the error is thrown immediately.
117+
- `error`: The error object that occurred.
118+
- `attempt`: The current attempt count (starting from 0).
89119

90120
#### Returns
91121

docs/zh_hans/reference/function/retry.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,33 @@ const data4 = await retry(
5252
);
5353
```
5454

55+
当只想在特定错误时重试时,可以使用 `shouldRetry` 选项。
56+
57+
```typescript
58+
import { retry } from 'es-toolkit/function';
59+
60+
class NetworkError extends Error {
61+
constructor(public status: number) {
62+
super(`Network error: ${status}`);
63+
}
64+
}
65+
66+
// 仅在 500+ 错误时重试
67+
const data5 = await retry(
68+
async () => {
69+
const response = await fetch('/api/data');
70+
if (!response.ok) {
71+
throw new NetworkError(response.status);
72+
}
73+
return response.json();
74+
},
75+
{
76+
retries: 3,
77+
shouldRetry: (error, attempt) => error instanceof NetworkError && error.status >= 500,
78+
}
79+
);
80+
```
81+
5582
也可以使用 AbortSignal 取消重试。
5683

5784
```typescript
@@ -86,6 +113,9 @@ try {
86113
- `retries` (`number`, 可选): 重试次数。默认值为 `Infinity`,无限重试。
87114
- `delay` (`number | (attempts: number) => number`, 可选): 重试间隔(毫秒)。可以使用数字或函数。默认值为 `0`
88115
- `signal` (`AbortSignal`, 可选): 可以取消重试的信号。
116+
- `shouldRetry` (`(error: unknown, attempt: number) => boolean`, 可选): 决定是否重试的函数。如果返回 `false`,则立即抛出错误。
117+
- `error`: 发生的错误对象。
118+
- `attempt`: 当前尝试次数 (从 0 开始)。
89119

90120
#### 返回值
91121

src/function/retry.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,38 @@ describe('retry', () => {
7575
);
7676
expect(func).toHaveBeenCalledTimes(0);
7777
});
78+
79+
it('should retry when shouldRetry returns true', async () => {
80+
const error = { status: 500, message: 'Server Error' };
81+
const func = vi.fn().mockRejectedValueOnce(error).mockResolvedValue('success');
82+
const shouldRetry = vi.fn((err: unknown) => (err as { status: number }).status >= 500);
83+
84+
const result = await retry(func, { retries: 3, shouldRetry });
85+
86+
expect(result).toBe('success');
87+
expect(func).toHaveBeenCalledTimes(2);
88+
expect(shouldRetry).toHaveBeenCalledWith(error, 0);
89+
});
90+
91+
it('should not retry when shouldRetry returns false', async () => {
92+
const error = { status: 400, message: 'Bad Request' };
93+
const func = vi.fn().mockRejectedValue(error);
94+
const shouldRetry = vi.fn((err: unknown) => (err as { status: number }).status >= 500);
95+
96+
await expect(retry(func, { retries: 3, shouldRetry })).rejects.toEqual(error);
97+
expect(func).toHaveBeenCalledTimes(1);
98+
expect(shouldRetry).toHaveBeenCalledWith(error, 0);
99+
});
100+
101+
it('should pass attempt number to shouldRetry', async () => {
102+
const error = new Error('failure');
103+
const func = vi.fn().mockRejectedValue(error);
104+
const shouldRetry = vi.fn((_err: unknown, attempt: number) => attempt < 2);
105+
106+
await expect(retry(func, { retries: 5, shouldRetry })).rejects.toThrow('failure');
107+
expect(func).toHaveBeenCalledTimes(3);
108+
expect(shouldRetry).toHaveBeenCalledWith(error, 0);
109+
expect(shouldRetry).toHaveBeenCalledWith(error, 1);
110+
expect(shouldRetry).toHaveBeenCalledWith(error, 2);
111+
});
78112
});

src/function/retry.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,24 @@ interface RetryOptions {
2121
* An AbortSignal to cancel the retry operation.
2222
*/
2323
signal?: AbortSignal;
24+
25+
/**
26+
* A function that determines whether to retry based on the error and attempt number.
27+
* If not provided, all errors will trigger a retry.
28+
*
29+
* @param {unknown} error - The error that occurred.
30+
* @param {number} attempt - The current attempt number (0-indexed).
31+
* @returns {boolean} Whether to retry.
32+
*
33+
* @example
34+
* shouldRetry: (error, attempt) => error.status >= 500
35+
*/
36+
shouldRetry?: (error: unknown, attempt: number) => boolean;
2437
}
2538

2639
const DEFAULT_DELAY = 0;
2740
const DEFAULT_RETRIES = Number.POSITIVE_INFINITY;
41+
const DEFAULT_SHOULD_RETRY = () => true;
2842

2943
/**
3044
* Retries a function that returns a promise until it resolves successfully.
@@ -62,6 +76,7 @@ export async function retry<T>(func: () => Promise<T>, retries: number): Promise
6276
* @param {number | ((attempts: number) => number)} [options.delay=0] - Delay(milliseconds) between retries.
6377
* @param {number} [options.retries=Infinity] - The number of retries to attempt.
6478
* @param {AbortSignal} [options.signal] - An AbortSignal to cancel the retry operation.
79+
* @param {(error: unknown, attempt: number) => boolean} [options.shouldRetry] - A function that determines whether to retry.
6580
* @returns {Promise<T>} A promise that resolves with the value of the successful function call.
6681
*
6782
* @example
@@ -96,15 +111,18 @@ export async function retry<T>(func: () => Promise<T>, _options?: number | Retry
96111
let delay: number | ((attempts: number) => number);
97112
let retries: number;
98113
let signal: AbortSignal | undefined;
114+
let shouldRetry: (error: unknown, attempt: number) => boolean;
99115

100116
if (typeof _options === 'number') {
101117
delay = DEFAULT_DELAY;
102118
retries = _options;
103119
signal = undefined;
120+
shouldRetry = DEFAULT_SHOULD_RETRY;
104121
} else {
105122
delay = _options?.delay ?? DEFAULT_DELAY;
106123
retries = _options?.retries ?? DEFAULT_RETRIES;
107124
signal = _options?.signal;
125+
shouldRetry = _options?.shouldRetry ?? DEFAULT_SHOULD_RETRY;
108126
}
109127

110128
let error;
@@ -119,6 +137,11 @@ export async function retry<T>(func: () => Promise<T>, _options?: number | Retry
119137
} catch (err) {
120138
error = err;
121139

140+
// Determine if we should retry based on the provided shouldRetry function
141+
if (!shouldRetry(err, attempts)) {
142+
throw err;
143+
}
144+
122145
const currentDelay = typeof delay === 'function' ? delay(attempts) : delay;
123146
await delayToolkit(currentDelay);
124147
}

0 commit comments

Comments
 (0)