JavaScriptのfetchでタイムアウトやエラーに対応 リトライも

こんにちは、さるまりんです🐒

Web APIとやりとりするときに使うfetch。とっても便利な関数ですが、単純に書くだけでは困ることも多いです。

通信が遅すぎて終わらなかったり、ネットワークが一時的に切れていたり、なぜか403 Forbiddenが返ってきてたり…。JavaScriptのエラー、わかりにくいことってありませんか?

こんなときにどう対処しているかをメモしておきます。

基本的なこと

fetchはこんなふうに書くと思います。

fetch('/api/data')
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error(err));

APIを呼び出して結果受け取り、成功していたら〇〇、失敗だったら〇〇。

これで基本的には良いのですが、ちょっとした工夫があるとより安心です。

タイムアウト処理を追加

いつまで経っても返ってこない…。そんな時のためにタイムアウト処理を追加します。

async function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeout);

  try {
    const res = await fetch(url, { signal: controller.signal });
    clearTimeout(timer);
    return await res.json();
  } catch (e) {
    if (e.name === 'AbortError') {
      console.error('リクエストがタイムアウトしました');
    } else {
      console.error('通信エラー:', e);
    }
    throw e;
  }
}

AbortControllerを使うとfetchを途中でキャンセルすることができます。↑ではtimeoutを5000に設定し、経過したらタイムアウトします。clearTimeout(timer);は呼び出しが時間内に成功していたら止めないようにタイマーをクリアしています。

リトライ処理(自前で再試行)

タイムアウトで終わるのとともによく使うのが、失敗していたら再度トライ、リトライの仕組みですね。こんな感じで実装しています。

async function fetchWithRetry(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const res = await fetch(url);
      if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
      return await res.json();
    } catch (e) {
      if (i === retries - 1) throw e;
      console.warn(`再試行中 (${i + 1}/${retries})...`);
      await new Promise(r => setTimeout(r, 1000));
    }
  }
}

fetchがエラーとなってしまった時は、forループを使って指定回数までは間隔をあけて再度アクセスしています。

並列 vs 順次処理

複数のAPIを呼び出すこともありますね。それを並列にするか順次にするかは場合によって変えますが、次のようにすることができるかと思います。

並列に複数fetch
const results = await Promise.all([
  fetch('/api/one'),
  fetch('/api/two')
]);
順番に1つずつfetch
for (const url of ['/api/one', '/api/two']) {
  const res = await fetch(url);
  const data = await res.json();
  console.log(data);
}

特定のステータスコードで処理を分岐

特定のエラーだけは別の処理をしたいこともありますよね。↓のようにやってみました。

async function fetchWithStatusHandling(url) {
  try {
    const res = await fetch(url);

    if (!res.ok) {
      if (res.status === 403) {
        console.warn(`403 Forbidden: ${url}`);
        showFriendlyError('この操作にはアクセスできません');
        return null;
      } else if (res.status === 401) {
        console.warn(`401 Unauthorized: ${url}`);
        showFriendlyError('ログインが必要です');
        return null;
      } else {
        throw new Error(`HTTP Error: ${res.status}`);
      }
    }

    return await res.json();
  } catch (e) {
    console.error('fetch失敗:', e);
    showFriendlyError('通信中にエラーが発生しました');
    return null;
  }
}

function showFriendlyError(msg) {
  alert(msg); // alertでエラーメッセージ
}

fetchでAPI呼び出しして、res.statusでステータスコードを取得。それが403 Forbiddenだったら?401 Unauthorizedだったら?そのほかの例外だったら?とメッセージの出し分けをしています。

showFriendlyError()がユーザーに対してメッセージを提供する部分ですが、ここではalert()呼び出ししてメッセージを表示しているだけですが、実際はもうちょっと凝ったUIとか入れたいですね。

fetchはシンプルです。シンプルなだけにそのままでは困ることもあるので、タイムアウトやリトライ、エラー判定をできるようにすると、エラーに強くユーザーにも優しいものにできると思います。

使いやすいのが一番ですね!

読んでくださってありがとうございました。

それではまた!