Skip to main content

第18章:失敗はどう扱う?(リトライOK/NGの分類)✅❌

この章でできるようになること 🎯✨

  • 「失敗」を リトライしていいものしちゃダメなもの に分けられるようになる 🙆‍♀️🙅‍♀️
  • HTTPステータスネットワーク失敗 を見て、次の一手(待つ/諦める/確認する)を決められる 🔍🧠
  • TypeScriptで「リトライの型」を作って、事故りにくい通信を実装できる 💻🔁

Concept

Concept


18.1 「失敗」はぜんぶ同じじゃない 😵‍💫➡️🧩

失敗にはざっくり3種類あるよ〜!🎀

① 一時的な失敗(リトライOK)🔁✅

「今たまたま無理だった」系。待ってもう一回で直ることが多い✨ 例:ネットワークの瞬断📶、タイムアウト⏱️、サーバー過負荷🏋️、ゲートウェイエラー🌉

② 恒久的な失敗(リトライNG)🚫❌

「原因が“入力や権限”にある」系。何回やっても同じ結果😇 例:入力ミス📝、認証エラー🔐、残高不足💸、存在しないID🔎

③ “結果がわからない”失敗(要注意)🤔⚠️

タイムアウト接続断 は特に危険! 「サーバー側では成功してたのに、返事だけ届かなかった」可能性があるから、無邪気に再送すると二重実行になりがち😱 ここは冪等性(Idempotency)と超相性がいいところだよ🔑✨


18.2 リトライの基本ルール 6つ 📏🧠🔁

ルール1:即リトライは禁止!まず “待つ” ⏳

Exponential Backoff + Jitter

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

Exponential Backoff and Jitter

ルール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 迷ったらこれ!リトライ判定フロー 🧭✨

  1. レスポンスがある?
  • ない → 「結果不明」扱い🤔⚠️(リトライするなら冪等キー前提が安全)
  • ある → 次へ
  1. Retry-After がある?
  • ある → その秒数/日時まで待つ🫡⏳ (rfc-editor.org)
  • ない → 次へ
  1. ステータスが 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.)
  • タイムアウト/接続断は 結果不明 になりやすいから、冪等キーと組み合わせると安全🔑✨