JavaScript | 非同期処理:fetch / API 通信 – 再試行処理

JavaScript JavaScript
スポンサーリンク

「再試行処理」を一言でいうと

fetch の「再試行処理」は、
「1 回の通信が失敗したときに、すぐ諦めずにもう何回かやり直す仕組み」 のことです。

ネットワークの世界では、
「たまたま一瞬だけ回線が不安定だった」
「サーバーが一瞬重かった」
といった“たまたまの失敗”がよくあります。

そんなときに、
「1 回失敗した=即エラー表示」ではなく、
「何回かリトライして、それでもダメならエラーにする」
というのが「再試行処理」です。

ここが重要です。
再試行処理は 「全部 retry すれば偉い」わけではありません。
どのエラーなら再試行して意味があるのか(ネットワーク一時的トラブルなど)、
どのエラーは再試行しても意味がないのか(パラメータが不正など)、
それを見分けた上で「上手に諦める」ための仕組みです。


まず前提:どんな失敗なら「再試行する意味がある」のか

再試行で改善しやすいエラー

ざっくりいうと、こんなタイプのエラーは再試行に向いています。

一時的なネットワークエラー
例: Wi-Fi が一瞬途切れた、回線が一瞬だけ不安定だった。

サーバーの一時的な負荷・不調
例: 503 Service Unavailable(今ちょっと無理)、
あるいはたまたま一瞬 500 が返ってきたが、すぐ復活しそうな状況。

こういうのは「時間を少し置いてもう一度叩けば成功する」可能性が高いので、
再試行が効果的です。

再試行しても無駄になりやすいエラー

逆に、何度やっても改善しないタイプのエラーもあります。

リクエスト内容が明らかに不正(400 Bad Request)
例: 必須パラメータが抜けている、型が違うなど。

認証・権限の問題(401, 403)
例: ログインしていない、トークンが失効している。

URL が間違っている 404 Not Found
例: エンドポイント自体が存在しない。

これらは 「リクエストをどう直すか」の問題 であって、
「回数を増やせば解決する」話ではありません。
再試行するより、早くユーザーや開発者に「内容がおかしい」と伝えるべきケースです。

ここが重要です。
再試行処理を設計するときは、
「どのエラーなら“待つ+やり直す”価値があるのか?」
「どのエラーは即座に諦めてユーザーに知らせるべきか?」

を切り分けることから始めると、無意味な retry 地獄を防げます。


一番シンプルな「固定回数リトライ」の形

基本アイデア

まずは「最大 3 回まで試して、全部ダメならエラー」という、固定回数リトライのテンプレートから。

雰囲気だけ先に見ると、こうです。

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  let lastError;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      if (!response.ok) {
        throw new Error("HTTPエラー: " + response.status);
      }

      return response; // 成功したらここで終了
    } catch (error) {
      lastError = error;
      console.warn(`試行 ${attempt} 回目で失敗:`, error.message);

      if (attempt === maxRetries) {
        break;
      }
    }
  }

  throw lastError;
}
JavaScript

コードを丁寧に分解する

for (let attempt = 1; attempt <= maxRetries; attempt++) { ... }
ここで「1 回目、2 回目、…」と試行回数を回しています。

const response = await fetch(url, options);
普通に fetch を呼ぶ。ネットワークエラーならここで throw されて catch に飛びます。

if (!response.ok) { throw new Error("HTTPエラー: " + response.status); }
404 や 500 など HTTP エラーも、自分で Error に変換して throw。
これで「fetch 自体の失敗」と「HTTP ステータスの失敗」を同じレイヤーで扱えるようになります。

return response;
どこかの試行で成功したら、その場で return して関数を終了。
その後の試行は行われません。

catch (error) { ... }
どこかで失敗した場合、ここでログを出しつつ、
attempt === maxRetries かどうかを見ます。

まだ試行回数が残っているなら、何もせずループを続行。
最大回数に達していたら break して throw lastError へ。

throw lastError;
最後に失敗したエラーを、そのまま呼び出し元に投げ返します。

使い方のイメージ

async function loadData() {
  try {
    const response = await fetchWithRetry("https://example.com/api/data");
    const data = await response.json();
    console.log("成功:", data);
  } catch (error) {
    console.error("最終的に失敗:", error.message);
  }
}
JavaScript

ここが重要です。
この「固定回数リトライ」は、
「多少の揺らぎには強くなるけれど、無限に retry してしまう危険はない」 という、安全な基本形です。
まずはこのパターンをしっかり理解して、自分で書けるようになることを目標にしてください。


再試行のたびに「少し待つ」:簡単な待機を入れる

連続して叩き続けると逆効果なこともある

固定回数リトライを、そのまま連続で叩き続けると、

「サーバーが一瞬重い」
「回線が一瞬不安定」

のタイミングで、
同じような失敗を高速で 3 回連続させてしまうことがあります。

それを避けるために、
「失敗したら、少し待ってから次の試行をする」 という待機を入れることがあります。

簡単な「一定時間待つ」版

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function fetchWithRetry(url, options = {}, maxRetries = 3, delayMs = 500) {
  let lastError;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      if (!response.ok) {
        throw new Error("HTTPエラー: " + response.status);
      }

      return response;
    } catch (error) {
      lastError = error;
      console.warn(`試行 ${attempt} 回目で失敗:`, error.message);

      if (attempt === maxRetries) {
        break;
      }

      await delay(delayMs); // ここで少し待つ
    }
  }

  throw lastError;
}
JavaScript

ここでは、

delay(ms) で「ms ミリ秒後に resolve される Promise」を作り、
await delay(delayMs) で実際に待機しています。

ここが重要です。
待機を入れるだけで、
「サーバーに連打しない」「一時的な揺らぎが収まる時間を与える」 という意味が生まれます。
ただ retry 回数を増やすより、「間隔を空ける」という発想もセットで持っておくと、より現実的な設計になります。


エラーの種類を見て「再試行するかどうか」を分ける

「何でもかんでも retry」しないためのフィルタ

さっきの実装だと、
HTTP ステータスが 400 や 401 のときも retry してしまいます。

これは現実的にはあまり嬉しくありません。

そこで、「再試行したいエラーだけ retry する」ように条件を足します。

例として、

  • ネットワークエラー(fetch が throw)
  • 500番台(サーバー側エラー)

のときだけ再試行し、
他(400番台など)は即座に失敗とみなす、という形にしてみます。

条件付きリトライのコード例

function shouldRetry(error, response) {
  if (error) {
    return true;
  }

  if (!response) {
    return false;
  }

  if (response.status >= 500 && response.status < 600) {
    return true;
  }

  return false;
}

async function fetchWithRetry(url, options = {}, maxRetries = 3, delayMs = 500) {
  let lastError;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    let response = null;
    let error = null;

    try {
      response = await fetch(url, options);

      if (!response.ok) {
        error = new Error("HTTPエラー: " + response.status);
      } else {
        return response;
      }
    } catch (err) {
      error = err;
    }

    lastError = error;
    console.warn(`試行 ${attempt} 回目で失敗:`, error.message);

    if (attempt === maxRetries || !shouldRetry(error, response)) {
      break;
    }

    await delay(delayMs);
  }

  throw lastError;
}
JavaScript

ここでのポイントは shouldRetry 関数です。

error がある(=ネットワークエラーなど)なら true。
response.status が 500〜599 なら true(サーバー側エラーとして再試行対象)。

それ以外(400番台など)のときは false を返して、
「このエラーは再試行しても意味がなさそうだから、ここで諦める」という判断をします。

ここが重要です。
再試行処理の質は、「何回やるか」ではなく、「どのエラーを retry 対象にするか」で決まる と言ってもいいです。
shouldRetry の中身を API や仕様に合わせて調整できるようになると、一気に“本番っぽい”コードになります。


再試行処理と UI の関係(ユーザーにどう見えるか)

ユーザーから見た「無限に待たされている感」をなくす

再試行処理を入れると、
「1 回の fetch で 3 回分の時間を使う」可能性があります。

例えば 1 回あたり 3 秒のタイムアウト × 3 回リトライ → 最大 9 秒。
その間ずっと「読み込み中」だけ出ていたら、ユーザーは不安になります。

そこで、UI 側ではこんな配慮が必要になります。

ローディング表示を出しつつ、「再試行中…」などの文言に変える。
一定回数失敗したら、「通信が不安定です。ネットワークを確認してから再度お試しください。」と明示する。
「再読み込み」ボタンを出して、ユーザーに主導権を返す。

実装イメージ(擬似コード)はこんな感じです。

async function loadWithRetryAndUI() {
  setLoading(true);
  setMessage("読み込み中...");

  try {
    const response = await fetchWithRetry("/api/data", {}, 3, 1000);
    const data = await response.json();
    showData(data);
    setMessage("");
  } catch (error) {
    setMessage("通信に失敗しました。ネットワークを確認して再度お試しください。");
  } finally {
    setLoading(false);
  }
}
JavaScript

ここが重要です。
再試行処理は、コードだけの話ではありません。
「その間ユーザーに何が見えて、何秒くらい待たされるのか」をセットで考える必要があります。
待たせるなら、何回目の試行なのか、どこまで頑張るのかを、テキストや UI で伝えると親切です。


初心者として「再試行処理」で本当に押さえてほしいこと

再試行処理は、「一度の失敗で諦めず、決めたルールの範囲でやり直す仕組み」。
単に回数を増やすのではなく、「どの種類の失敗なら retry に意味があるか」を考えるのが大事。

基本の形は「固定回数リトライ」。
for で試行回数を回し、try/catch の中で fetch → ok チェック → 成功なら return → 失敗なら次のループ、という流れを書く。

ネットワークエラーや 500番台のサーバーエラーなど、「一時的な問題」に対してだけ再試行する。
400番台や認証エラーなど、内容が間違っている場合は即座に諦める。

連続で叩き続けるのではなく、失敗ごとに await delay(ms) で少し待ってから次の試行をする。
サーバーやネットワークに「回復する時間」を与えるつもりで。

ユーザーの体験もセットで考える。
再試行中に何が表示されるか、最大で何秒待たせるか、
最終的に失敗したらどんなメッセージと「次の行動(再読み込みなど)」を提示するかを設計する。

ここが重要です。
再試行処理は、“ただの根性論” ではありません。
「どこまで頑張るか」「どこで諦めるか」を、自分で決められるようになる技術です。
コードを書くときには、
「これは一時的なエラーか?」
「もう一度やれば成功する可能性があるか?」
「ユーザーは何秒くらいなら許容してくれそうか?」
と自分に問いかけながら retry ロジックを組んでみてください。
その問いを重ねるほど、あなたの非同期処理は“現実に強い”ものになっていきます。

タイトルとURLをコピーしました