失敗したら再試行する

一時的な失敗だけをやり直します。回数で打ち切る書き方、待ち時間を伸ばす理由、やり直してはいけない失敗の見分け方、最後の失敗を握りつぶさない形までを実行して確かめます。

実行して検証済み処理系Node 22.22.3対応ES2017難易度●●●○○目安9更新

答え

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

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
1回目 失敗: 一時的 2回目 失敗: 一時的 成功 呼んだ回数: 3
example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
最後まで失敗: だめ

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

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

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

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
[ '前', '後' ]
example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
[ 200, 400, 800, 1600 ]

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

待ってからやり直す

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

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
やり直さない: 入力が不正 終了: 入力が不正
javascript
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 ? '失敗か、結果が無いのか分からない' : '成功');
null 失敗か、結果が無いのか分からない

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

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

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
[ 200, 400, 800, 1600, 3200, 5000 ]
example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
[ [ 429, true ], [ 500, true ], [ 503, true ], [ 400, false ], [ 401, false ], [ 404, false ] ]

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

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

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

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

失敗 やり直す?
通信が切れた する
429(多すぎる) する(待ってから)
500 502 503 する
400(送った内容が悪い) しない
401 403(権限が無い) しない
404(無い) しない

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

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
false true

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

よくある間違い

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

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

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

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

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

状態コードで決める

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

同時に走らせる数を抑える話は、こちらにまとめてあります。 → 配列を決まった数ずつに分ける

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

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

  • 読み出し(取得)… 何回やっても同じ。安全
  • 作成(登録・送金)… 2件できるかもしれない

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

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

打ち切りも決める

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

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

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

まとめ

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

再試行は、失敗が一時的なものだけに効きます。 そもそも何が失敗なのかを見分けるところから始めてください。 → SyntaxError: Unexpected end of JSON input

この記事の根拠

  1. Async Function Definitions — ECMAScript® 2026 Language Specificationtc39.es
  2. Promise Objects — ECMAScript® 2026 Language Specificationtc39.es
  3. AbortSignal — MDNdeveloper.mozilla.org

掲載しているコード例は、公開前に読み手が押したときに動くのと同じもので実行して出力を突き合わせています。結果はリポジトリに残しています。

このページの原文(Markdown)