JavaScriptのfetchAPIにタイムアウトを設定する方法10選

JavaScriptのfetchAPIにAbortSignal.timeoutを使ってタイムアウトを設定するJS
この記事は約37分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

●fetchAPIとタイムアウトの概要

JavaScriptのfetchAPIを使ってWebサービスと通信する際、タイムアウト処理は欠かせません。

○fetchAPIの基本的な使い方

fetchAPIは、サーバーとの通信を行うための強力なツールです。

URLを指定してfetch関数を呼び出すだけで、簡単にHTTPリクエストを送信できます。

レスポンスはPromiseで返されるので、非同期処理も扱いやすくなっています。

fetch('https://example.com/api/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

このコードを実行すると、指定したURLにGETリクエストが送信されます。

レスポンスが返ってきたら、jsonメソッドでJSONデータに変換し、コンソールに出力します。

エラーが発生した場合はcatchブロックで処理されます。

○タイムアウトが必要な理由

しかし、ネットワークの状態によっては、レスポンスが返ってこない場合があります。

そのままだと、アプリケーションが永遠に待ち続けてしまうことになります。

ユーザーにとっては、何も反応しないアプリケーションは不安になりますよね。

そこで、タイムアウト処理が必要になります。

一定時間レスポンスがない場合は、リクエストを中断してエラーハンドリングを行うことで、アプリケーションのUXを向上させることができるのです。

○AbortSignal.timeoutの役割

そこで登場するのが、AbortSignal.timeoutです。

これは、一定時間経過後にAbortSignalを発行するための便利なメソッドです。

fetchAPIにAbortSignalを渡すことで、リクエストをキャンセルすることができます。

つまり、AbortSignal.timeoutとfetchAPIを組み合わせることで、簡単にタイムアウト処理を実装できるというわけです。

●AbortSignal.timeoutを使った基本的なタイムアウト設定

さて、AbortSignal.timeoutを使ったタイムアウト設定の基本を見ていきましょう。

これは、fetchAPIを使う上で欠かせない知識ですからね。

○サンプルコード1:シンプルなタイムアウト設定

まずは、シンプルなタイムアウト設定のサンプルコードから始めましょう。

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);

fetch('https://example.com/api/data', { signal: controller.signal })
  .then(response => response.json())
  .then(data => {
    console.log(data);
    clearTimeout(timeoutId);
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      console.error('Request timed out');
    } else {
      console.error('Request failed', error);
    }
  });

このコードでは、AbortControllerを作成し、5秒後にabortメソッドを呼び出すようにsetTimeoutを設定しています。

そして、fetchの第2引数にcontroller.signalを渡すことで、タイムアウトが発生した際にリクエストを中止するようにしています。

リクエストが成功した場合は、clearTimeoutでタイムアウト用のタイマーをクリアしています。

一方、エラーが発生した場合は、error.nameが’AbortError’かどうかで、タイムアウトエラーとそれ以外のエラーを区別しています。

実行結果は、次のようになります。

  • リクエストが5秒以内に完了した場合
{ "message": "Data retrieved successfully" }
  • リクエストが5秒以内に完了しなかった場合
Request timed out

このように、AbortSignal.timeoutを使うことで、簡単にタイムアウト処理を実装できました。

ただ、エラーハンドリングがまだ不十分ですよね。

○サンプルコード2:タイムアウト時のエラーハンドリング

そこで、もう少し丁寧にエラーハンドリングを行ってみましょう。

const controller = new AbortController();
const timeoutId = setTimeout(() => {
  controller.abort();
  reject(new Error('Request timed out'));
}, 5000);

fetch('https://example.com/api/data', { signal: controller.signal })
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not OK');
    }
    return response.json();
  })
  .then(data => {
    console.log(data);
    clearTimeout(timeoutId);
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      console.error('Request aborted', error);
    } else {
      console.error('Request failed', error);
    }
  });

ここでは、setTimeoutのコールバック内で、abortメソッドを呼び出した後、rejectを使ってタイムアウトエラーを明示的に投げています。

また、レスポンスが成功した場合でも、response.okをチェックして、ステータスコードが200番台以外の場合はエラーを投げるようにしました。

こうすることで、ネットワークエラーとタイムアウトエラーを区別しつつ、適切にエラーハンドリングができるようになります。

実行結果は、次のようになります。

  • リクエストが5秒以内に完了し、レスポンスが成功した場合
{ "message": "Data retrieved successfully" }
  • リクエストが5秒以内に完了したが、レスポンスが失敗した場合
Request failed Error: Network response was not OK
  • リクエストが5秒以内に完了しなかった場合
Request aborted Error: Request timed out

○サンプルコード3:複数のリクエストにタイムアウトを設定

では、複数のリクエストにまとめてタイムアウトを設定する方法を見てみましょう。

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);

Promise.all([
  fetch('https://example.com/api/data1', { signal: controller.signal }),
  fetch('https://example.com/api/data2', { signal: controller.signal }),
  fetch('https://example.com/api/data3', { signal: controller.signal })
])
  .then(responses => Promise.all(responses.map(response => response.json())))
  .then(data => {
    console.log(data);
    clearTimeout(timeoutId);
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      console.error('Requests timed out');
    } else {
      console.error('Requests failed', error);
    }
  });

ここでは、Promise.allを使って複数のfetchリクエストを並列に実行しています。

そして、すべてのリクエストに同じAbortSignalを渡すことで、まとめてタイムアウト管理を行っています。

リクエストがすべて成功した場合は、それぞれのレスポンスをjsonメソッドで解析し、結果を配列として受け取ります。

一方、いずれかのリクエストがタイムアウトした場合は、AbortErrorが発生し、catchブロックで処理されます。

実行結果は、次のようになります。

  • すべてのリクエストが5秒以内に完了した場合
[
  { "message": "Data1 retrieved successfully" },
  { "message": "Data2 retrieved successfully" },
  { "message": "Data3 retrieved successfully" }
]
  • いずれかのリクエストが5秒以内に完了しなかった場合
Requests timed out

このように、AbortSignal.timeoutを使えば、複数のリクエストにも簡単にタイムアウト設定を行うことができます。

●AbortControllerを使った柔軟なタイムアウト設定

AbortSignal.timeoutを使えば、簡単にタイムアウト設定ができることがわかりましたね。

でも、もっと柔軟な設定がしたい場合はどうしましょう?

そこで、AbortControllerの出番です。

AbortControllerを使えば、任意のタイミングでリクエストを中止したり、複数のリクエストをグループ化して管理したりできるんです。

○サンプルコード4:AbortControllerの基本的な使い方

まずは、AbortControllerの基本的な使い方から見ていきましょう。

const controller = new AbortController();
const signal = controller.signal;

fetch('https://example.com/api/data', { signal })
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      console.error('Request aborted', error);
    } else {
      console.error('Request failed', error);
    }
  });

// リクエストを中止する
controller.abort();

ここでは、AbortControllerのインスタンスを作成し、そのsignalプロパティをfetchの第2引数に渡しています。

そして、controller.abort()を呼び出すことで、任意のタイミングでリクエストを中止しています。

リクエストが中止された場合、fetchはAbortErrorをrejectします。

これをcatchブロックで処理することで、リクエストが中止されたことを検知できます。

実行結果は、次のようになります。

Request aborted AbortError: The user aborted a request.

○サンプルコード5:AbortControllerを使った条件付きタイムアウト

次に、AbortControllerを使って条件付きのタイムアウトを設定してみましょう。

const controller = new AbortController();
const signal = controller.signal;

// 3秒以内にデータが取得できなければタイムアウト
const timeoutId = setTimeout(() => {
  controller.abort();
}, 3000);

fetch('https://example.com/api/data', { signal })
  .then(response => response.json())
  .then(data => {
    console.log(data);
    clearTimeout(timeoutId);
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      if (error.message === 'The operation was aborted.') {
        console.error('Request timed out');
      } else {
        console.error('Request aborted', error);
      }
    } else {
      console.error('Request failed', error);
    }
  });

ここでは、3秒以内にレスポンスが返ってこない場合に、AbortControllerを使ってリクエストを中止しています。

setTimeoutを使って3秒後にcontroller.abort()を呼び出すようにしています。

そして、レスポンスが成功した場合は、clearTimeoutでタイマーをクリアしています。

catchブロックでは、AbortErrorのメッセージを見て、タイムアウトによる中止なのか、それ以外の理由による中止なのかを区別しています。

実行結果は、次のようになります。

  • リクエストが3秒以内に完了した場合
{ "message": "Data retrieved successfully" }
  • リクエストが3秒以内に完了しなかった場合
Request timed out

○サンプルコード6:AbortControllerを使った動的なタイムアウト設定

では、もう少し実践的なケースとして、リクエストの状況に応じて動的にタイムアウト時間を変更する方法を見てみましょう。

function fetchWithDynamicTimeout(url, options = {}) {
  const controller = new AbortController();
  const signal = controller.signal;

  let timeoutId;
  let timeout = options.timeout || 5000;

  const fetchPromise = fetch(url, { ...options, signal });

  const timeoutPromise = new Promise((_, reject) => {
    timeoutId = setTimeout(() => {
      controller.abort();
      reject(new Error('Request timed out'));
    }, timeout);
  });

  return Promise.race([fetchPromise, timeoutPromise])
    .then(response => {
      clearTimeout(timeoutId);
      if (!response.ok) {
        throw new Error('Network response was not OK');
      }
      return response;
    })
    .catch(error => {
      if (error.name === 'AbortError') {
        if (options.retryCount > 0) {
          options.retryCount--;
          options.timeout *= 2;
          return fetchWithDynamicTimeout(url, options);
        }
        throw new Error('Request timed out');
      }
      throw error;
    });
}

fetchWithDynamicTimeout('https://example.com/api/data', { timeout: 3000, retryCount: 2 })
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error('Request failed', error);
  });

ここでは、fetchWithDynamicTimeoutという関数を定義しています。

この関数は、URLとオプションを受け取り、fetchPromiseとtimeoutPromiseのどちらか早く完了した方を返します。

timeoutPromiseは、指定されたタイムアウト時間が経過したらAbortErrorをrejectするPromiseです。

fetchPromiseは、通常のfetchのPromiseです。

Promise.raceを使って、この2つのPromiseのどちらか早く完了した方を返しています。

そして、レスポンスが成功した場合は、timeoutのクリアを行っています。

もしタイムアウトが発生した場合は、retryCountオプションを見て、リトライ回数が残っていれば、タイムアウト時間を2倍にしてリトライを行います。

リトライ回数が0になったら、タイムアウトエラーを投げます。

このようにすることで、ネットワークの状況に応じて動的にタイムアウト時間を調整し、適切なリトライ処理を行うことができます。

実行結果は、次のようになります。

  • リクエストが成功した場合
{ "message": "Data retrieved successfully" }
  • 1回目のタイムアウト後、2回目のリクエストが成功した場合
{ "message": "Data retrieved successfully" }
  • リトライが全て失敗した場合
Request failed Error: Request timed out

このように、AbortControllerを使えば、より柔軟で実践的なタイムアウト設定が可能になります。

状況に応じて適切な設定を行うことで、ユーザーエクスペリエンスの向上につなげましょう。

●Promiseを使った実践的なタイムアウト設定

さて、ここまでで、AbortSignal.timeoutとAbortControllerを使ったタイムアウト設定の方法を見てきました。

でも、もっと柔軟で実践的なタイムアウト設定がしたいですよね。

そこで、Promiseの出番です。

Promiseを使えば、タイムアウト処理とエラーハンドリングをより細かく制御できるんです。

では早速、サンプルコードを見ていきましょう。

○サンプルコード7:Promiseを使ったタイムアウトとエラーハンドリング

まずは、Promiseを使ったシンプルなタイムアウト設定の例から始めましょう。

function fetchWithTimeout(url, options = {}) {
  const { timeout = 5000 } = options;

  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  return fetch(url, { ...options, signal: controller.signal })
    .then(response => {
      clearTimeout(timeoutId);
      if (!response.ok) {
        throw new Error('Network response was not OK');
      }
      return response;
    })
    .catch(error => {
      if (error.name === 'AbortError') {
        throw new Error('Request timed out');
      }
      throw error;
    });
}

fetchWithTimeout('https://example.com/api/data', { timeout: 3000 })
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error('Request failed', error);
  });

ここでは、fetchWithTimeoutという関数を定義しています。

この関数は、URLとオプションを受け取り、AbortControllerを使ってタイムアウト設定を行ったfetchのPromiseを返します。

Promiseのthenメソッドを使って、レスポンスが成功した場合の処理を記述しています。

レスポンスのステータスコードが200番台以外の場合は、エラーを投げるようにしています。

また、catchメソッドを使って、エラーハンドリングを行っています。

AbortErrorが発生した場合は、タイムアウトエラーとして処理し、それ以外のエラーはそのまま再スローしています。

実行結果は、次のようになります。

  • リクエストが3秒以内に完了し、レスポンスが成功した場合
{ "message": "Data retrieved successfully" }
  • リクエストが3秒以内に完了したが、レスポンスが失敗した場合
Request failed Error: Network response was not OK
  • リクエストが3秒以内に完了しなかった場合
Request failed Error: Request timed out

○サンプルコード8:Promiseを使った複数リクエストのタイムアウト管理

次に、Promiseを使って複数のリクエストにまとめてタイムアウトを設定する方法を見てみましょう。

function fetchAllWithTimeout(urls, options = {}) {
  const { timeout = 5000 } = options;

  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  const fetchPromises = urls.map(url => fetch(url, { signal: controller.signal }));

  return Promise.all(fetchPromises)
    .then(responses => {
      clearTimeout(timeoutId);
      return Promise.all(responses.map(response => {
        if (!response.ok) {
          throw new Error('Network response was not OK');
        }
        return response.json();
      }));
    })
    .catch(error => {
      if (error.name === 'AbortError') {
        throw new Error('Requests timed out');
      }
      throw error;
    });
}

fetchAllWithTimeout([
  'https://example.com/api/data1',
  'https://example.com/api/data2',
  'https://example.com/api/data3'
], { timeout: 3000 })
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error('Requests failed', error);
  });

ここでは、fetchAllWithTimeoutという関数を定義しています。

この関数は、URLの配列とオプションを受け取り、すべてのリクエストにタイムアウト設定を行ったPromiseを返します。

URLの配列に対して、mapメソッドを使ってfetchのPromiseを作成しています。

そして、Promise.allを使って、すべてのPromiseが完了するのを待ちます。

レスポンスが成功した場合は、それぞれのレスポンスをjsonメソッドで解析し、結果の配列を返します。

一方、いずれかのリクエストがタイムアウトした場合は、AbortErrorが発生し、catchブロックで処理されます。

実行結果は、次のようになります。

  • すべてのリクエストが3秒以内に完了した場合
[
  { "message": "Data1 retrieved successfully" },
  { "message": "Data2 retrieved successfully" },
  { "message": "Data3 retrieved successfully" }
]
  • いずれかのリクエストが3秒以内に完了しなかった場合
Requests failed Error: Requests timed out

○サンプルコード9:Promiseを使った再試行可能なタイムアウト

最後に、Promiseを使ってリクエストが失敗した場合に再試行を行う方法を見てみましょう。

function fetchWithRetry(url, options = {}) {
  const { timeout = 5000, retryCount = 3, retryDelay = 1000 } = options;

  return new Promise((resolve, reject) => {
    let retries = 0;

    function attemptFetch() {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), timeout);

      fetch(url, { signal: controller.signal })
        .then(response => {
          clearTimeout(timeoutId);
          if (!response.ok) {
            throw new Error('Network response was not OK');
          }
          resolve(response);
        })
        .catch(error => {
          if (error.name === 'AbortError') {
            if (retries < retryCount) {
              retries++;
              setTimeout(attemptFetch, retryDelay);
            } else {
              reject(new Error('Request timed out'));
            }
          } else {
            reject(error);
          }
        });
    }

    attemptFetch();
  });
}

fetchWithRetry('https://example.com/api/data', { timeout: 3000, retryCount: 2, retryDelay: 1500 })
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error('Request failed', error);
  });

ここでは、fetchWithRetryという関数を定義しています。

この関数は、URLとオプションを受け取り、タイムアウトが発生した場合に指定された回数だけリトライを行うPromiseを返します。

新しいPromiseを作成し、その中でattemptFetchという関数を定義しています。

attemptFetch関数は、AbortControllerを使ってfetchを行い、レスポンスが成功した場合はresolveを呼び出します。

タイムアウトが発生した場合は、retryCountを確認し、残りのリトライ回数があればretryDelayミリ秒待ってからattemptFetchを再帰的に呼び出します。

リトライ回数が0になったら、タイムアウトエラーをrejectします。

実行結果は、次のようになります。

  • リクエストが成功した場合
{ "message": "Data retrieved successfully" }
  • 1回目のタイムアウト後、2回目のリクエストが成功した場合
{ "message": "Data retrieved successfully" }
  • リトライが全て失敗した場合
Request failed Error: Request timed out

このように、Promiseを使えば、タイムアウト設定とエラーハンドリングをより細かく制御できます。

再試行やフォールバック処理なども柔軟に実装できるので、実践的なタイムアウト管理に役立つでしょう。

●よくあるエラーと対処法

さて、ここまでfetchAPIでのタイムアウト設定について、様々な方法を見てきましたが、実際にコードを書いていると、エラーに遭遇することがありますよね。

そこで、このセクションでは、よくあるエラーとその対処法について解説していきます。

エラーが発生した時に慌てないように、しっかり理解しておきましょう。

○AbortErrorが発生した場合の対処法

まずは、AbortErrorについて見ていきましょう。

AbortErrorは、リクエストが中止された時に発生するエラーです。

AbortErrorが発生した場合、次のようなことを確認してみてください。

  • AbortControllerのシグナルがfetchに正しく渡されているか
  • タイムアウト時間が適切に設定されているか
  • controller.abort()が意図しないタイミングで呼び出されていないか

また、AbortErrorをキャッチした際は、次のように処理するのが一般的です。

fetch(url, { signal })
  .then(response => {
    // レスポンス処理
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Request was aborted');
    } else {
      console.error('Request failed', error);
    }
  });

ここでは、errorオブジェクトのnameプロパティを見て、AbortErrorかどうかを判定しています。

AbortErrorの場合は、リクエストが中止されたことをログに出力し、それ以外のエラーの場合は、通常のエラー処理を行っています。

実行結果は、次のようになります。

  • リクエストが中止された場合
Request was aborted
  • それ以外のエラーが発生した場合
Request failed Error: Network error

○ネットワークエラーとタイムアウトエラーの見分け方

次に、ネットワークエラーとタイムアウトエラーの見分け方について解説します。

ネットワークエラーは、ネットワークの問題やサーバーの障害などが原因で発生します。

一方、タイムアウトエラーは、指定した時間内にレスポンスが返ってこなかった場合に発生します。

これらのエラーを見分けるには、次のようにします。

fetch(url, { signal })
  .then(response => {
    // レスポンス処理
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      if (/^TimeoutError:/.test(error.message)) {
        console.log('Request timed out');
      } else {
        console.log('Request was aborted');
      }
    } else {
      console.error('Network error', error);
    }
  });

ここでは、AbortErrorの場合、さらにerror.messageを正規表現でチェックしています。

/^TimeoutError:/は、メッセージが”TimeoutError:”で始まる場合にマッチします。

これにより、タイムアウトエラーとそれ以外のAbortErrorを区別することができます。

実行結果は、次のようになります。

  • リクエストがタイムアウトした場合
Request timed out
  • リクエストが中止された場合(タイムアウト以外の理由)
Request was aborted
  • ネットワークエラーが発生した場合
Network error Error: Network error

○タイムアウト設定が機能しない場合のデバッグ方法

最後に、タイムアウト設定が機能しない場合のデバッグ方法を見ていきましょう。

タイムアウト設定が効かない場合、次のような点を確認してみてください。

  • AbortControllerのシグナルがfetchに正しく渡されているか
  • タイムアウト時間が適切に設定されているか
  • controller.abort()が正しいタイミングで呼び出されているか
  • レスポンスが返ってくるまでの時間が、想定よりも長くなっていないか

特に、最後の点は注意が必要です。

レスポンスが返ってくるまでの時間が長い場合、タイムアウト時間を長めに設定する必要があります。

また、デバッグの際は、次のようにログを出力すると良いでしょう。

const controller = new AbortController();
const signal = controller.signal;

const timeoutId = setTimeout(() => {
  console.log('Aborting request...');
  controller.abort();
}, timeout);

fetch(url, { signal })
  .then(response => {
    console.log('Response received:', response);
    // レスポンス処理
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Request aborted');
    } else {
      console.error('Request failed', error);
    }
  })
  .finally(() => {
    clearTimeout(timeoutId);
  });

ここでは、のようなログを出力しています。

  • controller.abort()が呼び出される直前に”Aborting request…”
  • レスポンスを受け取った時に”Response received:”
  • リクエストが中止された時に”Request aborted”
  • リクエストが失敗した時に”Request failed”

これにより、タイムアウト設定がどのように機能しているかを詳細に確認することができます。

実行結果は、次のようになります。

  • タイムアウトが発生した場合
Aborting request...
Request aborted
  • レスポンスが正常に受け取れた場合
Response received: Response { ... }
  • エラーが発生した場合
Request failed Error: Network error

このように、ログを活用することで、タイムアウト設定の動作を詳細に確認し、問題の原因を特定することができます。

●fetchAPIのタイムアウト設定のベストプラクティス

さて、ここまでfetchAPIでのタイムアウト設定について、様々な方法を見てきましたが、実際のプロジェクトで使うとなると、どのような設定が最適なのか悩むこともあるでしょう。

そこで、ここでは、fetchAPIのタイムアウト設定のベストプラクティスについて解説していきます。

実践で使えるテクニックを身につけて、より堅牢なWebアプリケーションを開発しましょう。

○サンプルコード10:実践で使えるタイムアウト設定のテンプレート

まずは、実践で使えるタイムアウト設定のテンプレートを見てみましょう。

async function fetchWithTimeout(url, options = {}) {
  const { timeout = 5000, ...fetchOptions } = options;

  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, { ...fetchOptions, signal: controller.signal });
    if (!response.ok) {
      throw new Error('Network response was not OK');
    }
    return response;
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error('Request timed out');
    }
    throw error;
  } finally {
    clearTimeout(timeoutId);
  }
}

try {
  const response = await fetchWithTimeout('https://example.com/api/data', { 
    timeout: 3000,
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ key: 'value' }),
  });
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error('Request failed', error);
}

このテンプレートでは、次のような工夫が施されています。

  • async/awaitを使って、コードの可読性を向上
  • デフォルトのタイムアウト時間を5秒に設定
  • fetchOptionsを分離して、fetchのオプションを柔軟に指定可能に
  • try…catch…finallyを使って、エラーハンドリングとリソースの解放を適切に実施
  • レスポンスのステータスコードをチェックし、適切にエラーハンドリング

実行結果は、次のようになります。

  • リクエストが成功した場合
{ "message": "Data retrieved successfully" }
  • タイムアウトエラーが発生した場合
Request failed Error: Request timed out
  • ネットワークエラーが発生した場合
Request failed Error: Network response was not OK

○適切なタイムアウト時間の選び方

次に、適切なタイムアウト時間の選び方について考えてみましょう。

タイムアウト時間を設定する際は、次のような点を考慮する必要があります。

  • APIのレスポンス時間の平均値と最大値
  • ネットワーク環境の状況
  • ユーザーの体感速度への影響
  • サーバーリソースの使用効率

一般的には、レスポンス時間の最大値よりも少し長めの時間をタイムアウト時間に設定するのが良いでしょう。

ただし、あまりにも長いタイムアウト時間を設定すると、ユーザーの体感速度が悪くなったり、サーバーリソースを無駄に使ってしまったりする可能性があります。

また、モバイルアプリケーションの場合は、ネットワーク環境の状況によってレスポンス時間が大きく変動することがあるので、柔軟にタイムアウト時間を調整できるようにしておくと良いでしょう。

○エラーハンドリングとログ出力の重要性

タイムアウト設定を行う際は、エラーハンドリングとログ出力も忘れずに実装しましょう。

適切にエラーをハンドリングすることで、予期せぬ動作を防ぎ、アプリケーションの安定性を高めることができます。

また、ログ出力を行うことで、問題が発生した際に原因の特定が容易になります。

次のようなエラーハンドリングとログ出力を行うのがおすすめです。

try {
  const response = await fetchWithTimeout(url, options);
  // レスポンス処理
} catch (error) {
  if (error.name === 'AbortError') {
    console.error('Request timed out', error);
    // タイムアウトエラー時の処理
  } else if (error instanceof TypeError) {
    console.error('Network error', error);
    // ネットワークエラー時の処理
  } else {
    console.error('Unknown error', error);
    // その他のエラー時の処理
  }
}

ここでは、エラーの種類に応じて、適切なログメッセージを出力し、エラー処理を行っています。

  • タイムアウトエラーの場合は、”Request timed out”というメッセージを出力
  • ネットワークエラーの場合は、”Network error”というメッセージを出力
  • それ以外のエラーの場合は、”Unknown error”というメッセージを出力

このように、エラーの種類に応じて適切にハンドリングを行うことで、アプリケーションの品質を高めることができます。

○ブラウザとNode.jsでのタイムアウト設定の違い

最後に、ブラウザとNode.jsでのタイムアウト設定の違いについて触れておきましょう。

基本的には、ブラウザとNode.jsでfetchAPIの使い方に大きな違いはありません。

ただし、次のような点に注意が必要です。

  • Node.jsではAbortControllerがグローバルオブジェクトではないため、requireで読み込む必要がある
  • Node.jsではfetchがグローバルオブジェクトではないため、node-fetchなどのライブラリを使う必要がある
  • ブラウザではService Workerを使ってタイムアウト処理を行うこともできる

特に、Service Workerを使ったタイムアウト処理は、ネットワークリクエストをインターセプトして柔軟な制御ができるので、高度なユースケースで活用できます。

ここでは、Service Workerを使ったタイムアウト処理の例を見てみましょう。

// Service Worker
self.addEventListener('fetch', event => {
  const timeoutId = setTimeout(() => {
    event.respondWith(new Response('Request timed out', { status: 504 }));
  }, 5000);

  event.respondWith(
    fetch(event.request)
      .then(response => {
        clearTimeout(timeoutId);
        return response;
      })
      .catch(error => {
        clearTimeout(timeoutId);
        throw error;
      })
  );
});

ここでは、fetchイベントをリッスンし、5秒以内にレスポンスが返ってこない場合は、タイムアウトエラーをレスポンスとして返しています。

Service Workerを使った処理は、ブラウザでしか動作しませんが、ネットワークリクエストに対して柔軟な制御ができるので、必要に応じて検討してみると良いでしょう。

まとめ

さて、ここまでfetchAPIでのタイムアウト設定について、基本的な使い方から実践的なテクニックまで、幅広く解説してきました。

AbortSignal.timeoutやAbortControllerを使えば、柔軟かつ堅牢なタイムアウト設定が可能になります。

また、Promiseを活用することで、エラーハンドリングや再試行処理なども実装できます。

適切なタイムアウト時間の選択、丁寧なエラーハンドリング、わかりやすいログ出力などを心がければ、ユーザーに快適な体験を提供できるでしょう。

フロントエンド開発において、ネットワーク通信は避けて通れない課題です。

通信が不安定な環境でも、しっかりとタイムアウト設定を行うことで、アプリケーションの品質を高めることができます。

本記事で紹介した内容を参考に、皆さんの開発するアプリケーションにベストなタイムアウト設定を実装してみてください。

きっと、ユーザーに喜ばれるアプリケーションになるはずです。

fetchAPIでのタイムアウト設定、ぜひマスターしておきましょう!