第18章:失敗はどう扱う?(リトライOK/NGの分類)✅❌
この章でできるようになること 🎯✨
- 「失敗」を リトライしていいもの/しちゃダメなもの に分けられるようになる 🙆♀️🙅♀️
- HTTPステータス と ネットワーク失敗 を見て、次の一手(待つ/諦める/確認する)を決められる 🔍🧠
- TypeScriptで「リトライの型」を作って、事故りにくい通信を実装できる 💻🔁


18.1 「失敗」はぜんぶ同じじゃない 😵💫➡️🧩
失敗にはざっくり3種類あるよ〜!🎀
① 一時的な失敗(リトライOK)🔁✅
「今たまたま無理だった」系。待ってもう一回で直ることが多い✨ 例:ネットワークの瞬断📶、タイムアウト⏱️、サーバー過負荷🏋️、ゲートウェイエラー🌉
② 恒久的な失敗(リトライNG)🚫❌
「原因が“入力や権限”にある」系。何回やっても同じ結果😇 例:入力ミス📝、認証エラー🔐、残高不足💸、存在しないID🔎
③ “結果がわからない”失敗(要注意)🤔⚠️
タイムアウト や 接続断 は特に危険! 「サーバー側では成功してたのに、返事だけ届かなかった」可能性があるから、無邪気に再送すると二重実行になりがち😱 ここは冪等性(Idempotency)と超相性がいいところだよ🔑✨
18.2 リトライの基本ルール 6つ 📏🧠🔁
ルール1:即リトライは禁止!まず “待つ” ⏳

連打すると、サーバーが余計に死ぬ(スローダウン→さらに失敗…)の無限ループになりがち😵💫 なので 指数バックオフ(exponential backoff) が王道だよ📈✨ (Amazon Web Services, Inc.)

ルール2:待ち時間に “ゆらぎ(jitter)” を入れる 🎲
みんなが同時に再試行すると「再試行の大渋滞」になるから、少しランダムにずらすのがコツ🎯 (Amazon Web Services, Inc.)
ルール3:回数上限は必須(無限リトライしない)🔁🔚
「最大3回まで」みたいに必ず止める🛑 (止めないと、障害時にアプリが永遠にリトライして地獄👹)
ルール4:Retry-After が来たら最優先で従う 🫡📨
サーバーが「○秒後に来てね」って教えてくれる合図✨
Retry-After は 秒数 か 日時 で来るよ⏱️🗓️ (rfc-editor.org)
ルール5:HTTP的に “リトライしやすい操作” を知る 🔁🌐
HTTPでは 冪等なメソッド(PUT/DELETE/安全なメソッドなど)は、通信失敗時に自動再試行しやすい性質として説明されてるよ🧠 「同じリクエストを繰り返しても、サーバー上の意図した効果が同じ」って定義される✨ (rfc-editor.org)
ルール6:POSTをリトライするなら “冪等キー” とセット 🔑💥
POSTは「作成=増える」ので、無対策で再送すると二重作成しやすい😱 だから Idempotency-Key を付ける(&サーバー側が同じキーなら同じ結果を返す)で安全にするよ🔐🔁
18.3 まずは “分類表” を持とう 📋✨
A) HTTPステータス別:リトライ判定(超よく使う版)🌐
| ステータス | ざっくり意味 | リトライ | どう動く? |
|---|---|---|---|
| 429 | リクエスト多すぎ💦 | ✅ | Retry-After があれば待ってから再試行(なければバックオフ) (MDNウェブドキュメント) |
| 503 | 一時的に利用不可🛠️ | ✅ | できれば Retry-After に従う(サーバー側も一時的用途&復旧見込みを載せるのが推奨) (MDNウェブドキュメント) |
| 500 | サーバー内部エラー🔥 | ✅(回数限定) | 1〜数回だけバックオフして再試行 |
| 502 | ゲートウェイ不調🌉 | ✅(回数限定) | バックオフして再試行 |
| 504 | ゲートウェイタイムアウト⏱️ | ✅(回数限定) | バックオフして再試行 |
| 408 | リクエストタイムアウト⌛ | ✅(回数限定) | バックオフして再試行(ただし結果不明扱い寄り) |
| 400/422 | 入力がダメ📝 | ❌ | リトライじゃなく修正(バリデーション) |
| 401/403 | 認証/権限がない🔐 | ❌ | ログイン/権限対応が先 |
| 404 | ないよ🔎 | ❌ | IDやURLを見直す |
| 409 | 競合⚔️ | △ | 状況次第:最新状態を取得してからやり直し、が多い(闇雲リトライはNG) |
ポイント:429/503は “待て” が明示される可能性が高いから、まず
Retry-Afterを探すのが勝ち筋だよ🏆 (rfc-editor.org)
B) “HTTPレスポンスが取れない”系(ネットワーク失敗)📶
| 失敗 | 例 | リトライ | 注意 |
|---|---|---|---|
| タイムアウト | 応答が来ない | ✅(回数限定) | 結果不明 になりやすいので、冪等キーがあると安心🔑⚠️ |
| 接続リセット/切断 | ECONNRESET など | ✅(回数限定) | サーバー側は処理済みの可能性あり |
| DNS失敗 | 名前解決できない | ✅(少し待って) | ネット設定/一時障害の可能性 |
| クライアント側のキャンセル | Abort | △ | 自分で止めたなら、まず原因を確認 |
18.4 迷ったらこれ!リトライ判定フロー 🧭✨
- レスポンスがある?
- ない → 「結果不明」扱い🤔⚠️(リトライするなら冪等キー前提が安全)
- ある → 次へ
Retry-Afterがある?
- ある → その秒数/日時まで待つ🫡⏳ (rfc-editor.org)
- ない → 次へ
- ステータスが 429/503/5xx/408?
- はい → バックオフ+回数制限でリトライ🔁📈
- いいえ(4xx中心)→ リトライしない🙅♀️(入力・認証・仕様の問題)
18.5 TypeScriptミニ実装:安全寄りリトライ retryFetch 🧪💻🔁
目標 🎯
- 429/503 は
Retry-After優先 - それ以外の一時系は「指数バックオフ+ジッター」
- 4xxは基本リトライしない
- 回数上限&タイムアウトつき
type RetryOptions = {
maxAttempts: number; // 例: 4(= 初回 + リトライ3回)
timeoutMs: number; // 例: 8000
baseDelayMs: number; // 例: 300
maxDelayMs: number; // 例: 8000
};
const sleep = (ms: number) => new Promise<void>(r => setTimeout(r, ms));
/**
* 指数バックオフ + ジッター(ちょいランダム)
*/
function calcBackoffMs(attemptIndex: number, baseDelayMs: number, maxDelayMs: number) {
// attemptIndex: 1,2,3...(リトライ回数)
const exp = baseDelayMs * Math.pow(2, attemptIndex - 1);
const capped = Math.min(exp, maxDelayMs);
const jitter = Math.random() * 0.3 * capped; // 0〜30%ゆらぎ
return Math.floor(capped * 0.7 + jitter); // 70%〜100%くらい
}
/**
* Retry-After をミリ秒に変換(秒数 or HTTP-date)
* 仕様として秒数 or 日付が許される :contentReference[oaicite:8]{index=8}
*/
function parseRetryAfterMs(value: string | null): number | null {
if (!value) return null;
// 秒数(例: "120")
if (/^\d+$/.test(value.trim())) {
return Number(value.trim()) * 1000;
}
// HTTP-date(例: "Fri, 31 Dec 1999 23:59:59 GMT")
const t = Date.parse(value);
if (!Number.isNaN(t)) {
const diff = t - Date.now();
return diff > 0 ? diff : 0;
}
return null;
}
function isRetryableStatus(status: number) {
// 429, 503, 500, 502, 504, 408 などを対象にする
if (status === 429) return true;
if (status === 503) return true;
if (status === 408) return true;
if (status >= 500 && status <= 599) return true;
return false;
}
/**
* fetch を安全寄りにリトライする
*/
export async function retryFetch(
input: RequestInfo | URL,
init: RequestInit,
opt: RetryOptions
): Promise<Response> {
let lastError: unknown = null;
for (let attempt = 1; attempt <= opt.maxAttempts; attempt++) {
const ac = new AbortController();
const timeoutId = setTimeout(() => ac.abort(), opt.timeoutMs);
try {
const res = await fetch(input, { ...init, signal: ac.signal });
clearTimeout(timeoutId);
if (res.ok) return res;
// リトライ対象ステータスじゃないなら終了(4xx中心)
if (!isRetryableStatus(res.status)) return res;
// Retry-After があれば最優先で待つ
const raMs = parseRetryAfterMs(res.headers.get("Retry-After"));
if (raMs !== null) {
await sleep(raMs);
continue;
}
// なければバックオフ
if (attempt < opt.maxAttempts) {
const waitMs = calcBackoffMs(attempt, opt.baseDelayMs, opt.maxDelayMs);
await sleep(waitMs);
continue;
}
return res; // ここまで来たら諦め(最後のレスポンスを返す)
} catch (e) {
clearTimeout(timeoutId);
lastError = e;
// ネットワーク失敗・タイムアウト等:回数限定でリトライ
if (attempt < opt.maxAttempts) {
const waitMs = calcBackoffMs(attempt, opt.baseDelayMs, opt.maxDelayMs);
await sleep(waitMs);
continue;
}
throw lastError;
}
}
// ここには来ない想定だけど保険
throw lastError ?? new Error("retryFetch failed");
}
使い方イメージ(注文作成APIを叩く)🧾💳🔑
「結果不明」が起こりやすいPOSTこそ、冪等キーで守るのが安心だよ🛡️✨
import { retryFetch } from "./retryFetch";
import { randomUUID } from "node:crypto";
const idempotencyKey = randomUUID();
const res = await retryFetch("http://localhost:3000/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({ itemId: "cake-001", qty: 1 }),
}, {
maxAttempts: 4,
timeoutMs: 8000,
baseDelayMs: 300,
maxDelayMs: 8000,
});
if (res.ok) {
const body = await res.json();
console.log("OK:", body);
} else {
console.log("NG:", res.status, await res.text());
}
18.6 ミニ演習 📝✨(表を作るよ!)
演習1:エラー分類表を作ろう📋✍️
次の失敗を「リトライOK/NG/条件付き」に分類してね🎀
- 429(レート制限)📉
- 503(メンテ中/過負荷)🛠️
- 400(バリデーションエラー)📝
- タイムアウト(応答が来ない)⏱️
- 409(競合)⚔️
👉 それぞれ「次に何をするか」も1行で書こう(待つ?修正?状態確認?)🔍✨
演習2:retryFetch を自分用にチューニング🎛️
maxAttemptsを 3 にした版/5 にした版で体感の違いをメモ📝baseDelayMsを 200/500 で変えて、通信が混むときの“優しさ”を比べる🐢🐇
18.7 🤖AI活用(この章向け)💬✨
プロンプト1:分類の穴あきチェック🔍
「このリトライ分類表を作ったんだけど、実務で抜けやすい例を追加して。特にHTTP 429/503/5xx と ネットワークタイムアウト周りで。」
プロンプト2:自分のAPI仕様に当てはめ🧾
「私のAPI(注文作成/支払い確定)で起こりうる失敗を10個挙げて、リトライOK/NG/条件付きに分類して。条件付きの場合は“何を確認してから”かも書いて。」
プロンプト3:Retry-After の扱い確認🕰️
「Retry-After が秒数とHTTP-date両方あると聞いた。実装で気をつける点とテストケース案を出して。」
(Retry-After が秒数or日時で来るのは仕様として明確だよ📌) (rfc-editor.org)
よくある事故ポイント集 😱🧯
- 4xx(入力ミス)をリトライして永遠に失敗 🌀🙅♀️
- 即リトライでサーバーを追い込む(障害を悪化させる)🔥
Retry-Afterを無視してBANされる 🥶(429のとき特に) (MDNウェブドキュメント)- タイムアウトを“失敗確定”と思い込んで二重作成 🧨(結果不明なのが怖い)
まとめ 🌸✅
- リトライしていいのは「一時的」な失敗だけ🔁
- 429/503は
Retry-Afterを最優先で尊重🫡 (rfc-editor.org) - リトライは バックオフ+ジッター+回数上限 が基本📈🎲 (Amazon Web Services, Inc.)
- タイムアウト/接続断は 結果不明 になりやすいから、冪等キーと組み合わせると安全🔑✨