---
type: how-to
language: javascript
slug: async/retry
title: "失敗したら再試行する"
title_tag: "JavaScript でリトライ処理を書く — 待ち時間と打ち切り"
summary: >
  一時的な失敗だけをやり直します。回数で打ち切る書き方、待ち時間を伸ばす理由、
  やり直してはいけない失敗の見分け方、最後の失敗を握りつぶさない形までを実行して確かめます。
description: >
  何度やっても同じ失敗をやり直すのは無駄で有害です。やり直してよい失敗の見分け方、
  待ち時間を倍にする理由、最後の失敗を投げ直す形まで、回数を数えて確かめられます。
status: published
difficulty: 3
minutes: 9

versions:
  verified: "Node 22.22.3"
  since: "ES2017"
  deprecated: null
  removed: null

sources:
  - title: "Async Function Definitions — ECMAScript® 2026 Language Specification"
    url: "https://tc39.es/ecma262/#sec-async-function-definitions"
  - title: "Promise Objects — ECMAScript® 2026 Language Specification"
    url: "https://tc39.es/ecma262/#sec-promise-objects"
  - title: "AbortSignal — MDN"
    url: "https://developer.mozilla.org/ja/docs/Web/API/AbortSignal"

terms: [再試行, 指数バックオフ, 冪等, 打ち切り]

links:
  related:
    - javascript/reference/promise/promise
    - javascript/how-to/async/parallel
    - javascript/errors/unexpected-end-of-json

content_updated_at: 2026-09-08
published_at: 2026-09-08
---

## 答え

**回数で打ち切る `for` を書きます。** 最後の失敗は投げ直します。

```js run
async function retry(run, times) {
  let lastError;

  for (let attempt = 1; attempt <= times; attempt++) {
    try {
      return await run(attempt);
    } catch (error) {
      lastError = error;
      console.log(attempt + '回目 失敗: ' + error.message);
    }
  }

  throw lastError;
}

let calls = 0;

const flaky = async () => {
  calls++;
  if (calls < 3) throw new Error('一時的');
  return '成功';
};

console.log(await retry(flaky, 5));
console.log('呼んだ回数:', calls);
```
```output
1回目 失敗: 一時的
2回目 失敗: 一時的
成功
呼んだ回数: 3
```

**成功した時点で止まります。** 5回まで許していても、3回で終わっています。

## 最後の失敗を握りつぶさない

全部失敗したら、**最後の失敗をそのまま投げます。**

```js run
async function retry(run, times) {
  let lastError;

  for (let attempt = 1; attempt <= times; attempt++) {
    try {
      return await run();
    } catch (error) {
      lastError = error;
    }
  }

  throw lastError;
}

try {
  await retry(async () => {
    throw new Error('だめ');
  }, 3);
} catch (error) {
  console.log('最後まで失敗:', error.message);
}
```
```output
最後まで失敗: だめ
```

[bad:`return null` で終わらせないでください。] 呼び出し側が
「失敗した」のか「結果が無かった」のかを区別できなくなります。

## 待ってからやり直す

すぐにやり直しても、相手の状況は変わっていません。**待ち時間を置きます。**

```js run
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const order = [];

order.push('前');
await sleep(0);
order.push('後');

console.log(order);
```
```output
[ '前', '後' ]
```

`sleep()` は自分で書きます。標準にはありません。
`new Promise` を書くのは、こういう「`Promise` でない仕組みを包む」ときだけです。
→ [Promise — then / catch / finally と async / await](/ja/javascript/reference/promise/promise/)

待ち時間は[key:回ごとに伸ばします]。これを[type:指数バックオフ]と呼びます。

```js run
function waitMs(attempt) {
  return 200 * 2 ** (attempt - 1);
}

console.log([1, 2, 3, 4].map(waitMs));
```
```output
[ 200, 400, 800, 1600 ]
```

**なぜ伸ばすのか。** 相手が混んでいるときに全員が同じ間隔で叩き続けると、
混雑がいつまでも解けません。**下がってやり直すのが、結果として速くなります。**

[dim:実務では、ここに少しのばらつき（ジッター）を足します。同時に失敗した全員が同じ時刻に再開すると、また同時に殺到するためです。]

## やり直してはいけない失敗がある

**これがいちばん大事です。** 何度やっても同じ結果になる失敗をやり直すのは、
無駄なだけでなく[bad:相手への攻撃になります]。

| 失敗 | やり直す？ |
|---|---|
| 通信が切れた | **する** |
| `429`（多すぎる） | **する**（待ってから） |
| `500` `502` `503` | **する** |
| `400`（送った内容が悪い） | [bad:しない] |
| `401` `403`（権限が無い） | [bad:しない] |
| `404`（無い） | [bad:しない] |

**判定を渡せるようにしておきます。**

```js run
async function retry(run, { times, canRetry }) {
  let lastError;

  for (let attempt = 1; attempt <= times; attempt++) {
    try {
      return await run();
    } catch (error) {
      lastError = error;

      if (!canRetry(error)) {
        console.log('やり直さない:', error.message);
        throw error;
      }

      console.log(attempt + '回目 やり直す');
    }
  }

  throw lastError;
}

const canRetry = (error) => error.message !== '入力が不正';

try {
  await retry(async () => {
    throw new Error('入力が不正');
  }, { times: 3, canRetry });
} catch (error) {
  console.log('終了:', error.message);
}
```
```output
やり直さない: 入力が不正
終了: 入力が不正
```

**1回で止まりました。** 直らない失敗を3回繰り返しても意味がありません。

## よくある間違い

### 失敗を握りつぶして `null` を返す

```js bad
async function badRetry(run, times) {
  for (let i = 0; i < times; i++) {
    try {
      return await run();
    } catch {
      // 何もしない
    }
  }

  return null;
}

const out = await badRetry(async () => {
  throw new Error('通信できない');
}, 3);

console.log(out);
console.log(out === null ? '失敗か、結果が無いのか分からない' : '成功');
```
```output
null
失敗か、結果が無いのか分からない
```

**呼び出し側で区別できません。** 何が起きたのかも分かりません。
`catch {}` を空にするのは、**問題を消しているだけ**です。

### 上限を決めずに待ち時間を伸ばす

`2 ** attempt` は、回を重ねると一気に大きくなります。**頭を打たせてください。**

```js run
function waitMs(attempt, base = 200, max = 5000) {
  return Math.min(base * 2 ** (attempt - 1), max);
}

console.log([1, 2, 3, 4, 5, 6].map((n) => waitMs(n)));
```
```output
[ 200, 400, 800, 1600, 3200, 5000 ]
```

上限が無いと、6回目で[num:6400]ミリ秒、10回目には[num:100]秒を超えます。
**利用者が待てる時間には限りがあります。**

## 状態コードで決める

```js run
function canRetry(status) {
  return status === 429 || status >= 500;
}

console.log([429, 500, 503, 400, 401, 404].map((code) => [code, canRetry(code)]));
```
```output
[ [ 429, true ], [ 500, true ], [ 503, true ], [ 400, false ], [ 401, false ], [ 404, false ] ]
```

**判定を1か所の関数にまとめておく**と、あとで変えやすくなります。

同時に走らせる数を抑える話は、こちらにまとめてあります。
→ [配列を決まった数ずつに分ける](/ja/javascript/how-to/array/chunk/)

## 同じ操作を2回してよいか

やり直すということは、**同じ操作が2回実行されうる**ということです。

- 読み出し（取得）… 何回やっても同じ。**安全**
- 作成（登録・送金）… [bad:2件できるかもしれない]

「何回実行しても結果が同じ」ことを[type:冪等]と言います。
**冪等でない操作をやり直すときは、同じ操作だと相手に伝える印**
（同じ識別子を毎回送るなど）**が要ります。**

[em:再試行を足す前に、その操作が2回起きても大丈夫かを確かめてください。]

## 打ち切りも決める

回数だけでなく、**全体の時間**でも止められるようにしておくと安全です。

```js run
function shouldStop(startedAt, now, limitMs) {
  return now - startedAt >= limitMs;
}

console.log(shouldStop(1000, 1500, 3000));
console.log(shouldStop(1000, 4500, 3000));
```
```output
false
true
```

[dim:実際の時刻は実行のたびに変わるので、ここでは値を渡して判定だけを見せています。]

通信そのものにも上限を付けてください。`fetch()` なら `AbortSignal.timeout()` が使えます。
**やり直す前に、1回ずつを必ず終わらせる**のが前提です。

## まとめ

- 回数で打ち切る `for`。**成功したらその場で返す**
- 最後の失敗は[em:投げ直す]。`null` で握りつぶさない
- 待ち時間は回ごとに伸ばす（[type:指数バックオフ]）
- **直らない失敗はやり直さない。** `400` `401` `403` `404` は1回で止める
- **2回実行されても大丈夫か**を先に確かめる
- 回数だけでなく全体の時間でも打ち切る

再試行は、失敗が[key:一時的なものだけ]に効きます。
そもそも何が失敗なのかを見分けるところから始めてください。
→ [SyntaxError: Unexpected end of JSON input](/ja/javascript/errors/unexpected-end-of-json/)
