●fetchAPIとタイムアウトの概要
JavaScriptのfetchAPIを使ってWebサービスと通信する際、タイムアウト処理は欠かせません。
○fetchAPIの基本的な使い方
fetchAPIは、サーバーとの通信を行うための強力なツールです。
URLを指定してfetch関数を呼び出すだけで、簡単にHTTPリクエストを送信できます。
レスポンスはPromiseで返されるので、非同期処理も扱いやすくなっています。
このコードを実行すると、指定したURLにGETリクエストが送信されます。
レスポンスが返ってきたら、jsonメソッドでJSONデータに変換し、コンソールに出力します。
エラーが発生した場合はcatchブロックで処理されます。
○タイムアウトが必要な理由
しかし、ネットワークの状態によっては、レスポンスが返ってこない場合があります。
そのままだと、アプリケーションが永遠に待ち続けてしまうことになります。
ユーザーにとっては、何も反応しないアプリケーションは不安になりますよね。
そこで、タイムアウト処理が必要になります。
一定時間レスポンスがない場合は、リクエストを中断してエラーハンドリングを行うことで、アプリケーションのUXを向上させることができるのです。
○AbortSignal.timeoutの役割
そこで登場するのが、AbortSignal.timeoutです。
これは、一定時間経過後にAbortSignalを発行するための便利なメソッドです。
fetchAPIにAbortSignalを渡すことで、リクエストをキャンセルすることができます。
つまり、AbortSignal.timeoutとfetchAPIを組み合わせることで、簡単にタイムアウト処理を実装できるというわけです。
●AbortSignal.timeoutを使った基本的なタイムアウト設定
さて、AbortSignal.timeoutを使ったタイムアウト設定の基本を見ていきましょう。
これは、fetchAPIを使う上で欠かせない知識ですからね。
○サンプルコード1:シンプルなタイムアウト設定
まずは、シンプルなタイムアウト設定のサンプルコードから始めましょう。
このコードでは、AbortControllerを作成し、5秒後にabortメソッドを呼び出すようにsetTimeoutを設定しています。
そして、fetchの第2引数にcontroller.signalを渡すことで、タイムアウトが発生した際にリクエストを中止するようにしています。
リクエストが成功した場合は、clearTimeoutでタイムアウト用のタイマーをクリアしています。
一方、エラーが発生した場合は、error.nameが’AbortError’かどうかで、タイムアウトエラーとそれ以外のエラーを区別しています。
実行結果は、次のようになります。
- リクエストが5秒以内に完了した場合
- リクエストが5秒以内に完了しなかった場合
このように、AbortSignal.timeoutを使うことで、簡単にタイムアウト処理を実装できました。
ただ、エラーハンドリングがまだ不十分ですよね。
○サンプルコード2:タイムアウト時のエラーハンドリング
そこで、もう少し丁寧にエラーハンドリングを行ってみましょう。
ここでは、setTimeoutのコールバック内で、abortメソッドを呼び出した後、rejectを使ってタイムアウトエラーを明示的に投げています。
また、レスポンスが成功した場合でも、response.okをチェックして、ステータスコードが200番台以外の場合はエラーを投げるようにしました。
こうすることで、ネットワークエラーとタイムアウトエラーを区別しつつ、適切にエラーハンドリングができるようになります。
実行結果は、次のようになります。
- リクエストが5秒以内に完了し、レスポンスが成功した場合
- リクエストが5秒以内に完了したが、レスポンスが失敗した場合
- リクエストが5秒以内に完了しなかった場合
○サンプルコード3:複数のリクエストにタイムアウトを設定
では、複数のリクエストにまとめてタイムアウトを設定する方法を見てみましょう。
ここでは、Promise.allを使って複数のfetchリクエストを並列に実行しています。
そして、すべてのリクエストに同じAbortSignalを渡すことで、まとめてタイムアウト管理を行っています。
リクエストがすべて成功した場合は、それぞれのレスポンスをjsonメソッドで解析し、結果を配列として受け取ります。
一方、いずれかのリクエストがタイムアウトした場合は、AbortErrorが発生し、catchブロックで処理されます。
実行結果は、次のようになります。
- すべてのリクエストが5秒以内に完了した場合
- いずれかのリクエストが5秒以内に完了しなかった場合
このように、AbortSignal.timeoutを使えば、複数のリクエストにも簡単にタイムアウト設定を行うことができます。
●AbortControllerを使った柔軟なタイムアウト設定
AbortSignal.timeoutを使えば、簡単にタイムアウト設定ができることがわかりましたね。
でも、もっと柔軟な設定がしたい場合はどうしましょう?
そこで、AbortControllerの出番です。
AbortControllerを使えば、任意のタイミングでリクエストを中止したり、複数のリクエストをグループ化して管理したりできるんです。
○サンプルコード4:AbortControllerの基本的な使い方
まずは、AbortControllerの基本的な使い方から見ていきましょう。
ここでは、AbortControllerのインスタンスを作成し、そのsignalプロパティをfetchの第2引数に渡しています。
そして、controller.abort()を呼び出すことで、任意のタイミングでリクエストを中止しています。
リクエストが中止された場合、fetchはAbortErrorをrejectします。
これをcatchブロックで処理することで、リクエストが中止されたことを検知できます。
実行結果は、次のようになります。
○サンプルコード5:AbortControllerを使った条件付きタイムアウト
次に、AbortControllerを使って条件付きのタイムアウトを設定してみましょう。
ここでは、3秒以内にレスポンスが返ってこない場合に、AbortControllerを使ってリクエストを中止しています。
setTimeoutを使って3秒後にcontroller.abort()を呼び出すようにしています。
そして、レスポンスが成功した場合は、clearTimeoutでタイマーをクリアしています。
catchブロックでは、AbortErrorのメッセージを見て、タイムアウトによる中止なのか、それ以外の理由による中止なのかを区別しています。
実行結果は、次のようになります。
- リクエストが3秒以内に完了した場合
- リクエストが3秒以内に完了しなかった場合
○サンプルコード6:AbortControllerを使った動的なタイムアウト設定
では、もう少し実践的なケースとして、リクエストの状況に応じて動的にタイムアウト時間を変更する方法を見てみましょう。
ここでは、fetchWithDynamicTimeoutという関数を定義しています。
この関数は、URLとオプションを受け取り、fetchPromiseとtimeoutPromiseのどちらか早く完了した方を返します。
timeoutPromiseは、指定されたタイムアウト時間が経過したらAbortErrorをrejectするPromiseです。
fetchPromiseは、通常のfetchのPromiseです。
Promise.raceを使って、この2つのPromiseのどちらか早く完了した方を返しています。
そして、レスポンスが成功した場合は、timeoutのクリアを行っています。
もしタイムアウトが発生した場合は、retryCountオプションを見て、リトライ回数が残っていれば、タイムアウト時間を2倍にしてリトライを行います。
リトライ回数が0になったら、タイムアウトエラーを投げます。
このようにすることで、ネットワークの状況に応じて動的にタイムアウト時間を調整し、適切なリトライ処理を行うことができます。
実行結果は、次のようになります。
- リクエストが成功した場合
- 1回目のタイムアウト後、2回目のリクエストが成功した場合
- リトライが全て失敗した場合
このように、AbortControllerを使えば、より柔軟で実践的なタイムアウト設定が可能になります。
状況に応じて適切な設定を行うことで、ユーザーエクスペリエンスの向上につなげましょう。
●Promiseを使った実践的なタイムアウト設定
さて、ここまでで、AbortSignal.timeoutとAbortControllerを使ったタイムアウト設定の方法を見てきました。
でも、もっと柔軟で実践的なタイムアウト設定がしたいですよね。
そこで、Promiseの出番です。
Promiseを使えば、タイムアウト処理とエラーハンドリングをより細かく制御できるんです。
では早速、サンプルコードを見ていきましょう。
○サンプルコード7:Promiseを使ったタイムアウトとエラーハンドリング
まずは、Promiseを使ったシンプルなタイムアウト設定の例から始めましょう。
ここでは、fetchWithTimeoutという関数を定義しています。
この関数は、URLとオプションを受け取り、AbortControllerを使ってタイムアウト設定を行ったfetchのPromiseを返します。
Promiseのthenメソッドを使って、レスポンスが成功した場合の処理を記述しています。
レスポンスのステータスコードが200番台以外の場合は、エラーを投げるようにしています。
また、catchメソッドを使って、エラーハンドリングを行っています。
AbortErrorが発生した場合は、タイムアウトエラーとして処理し、それ以外のエラーはそのまま再スローしています。
実行結果は、次のようになります。
- リクエストが3秒以内に完了し、レスポンスが成功した場合
- リクエストが3秒以内に完了したが、レスポンスが失敗した場合
- リクエストが3秒以内に完了しなかった場合
○サンプルコード8:Promiseを使った複数リクエストのタイムアウト管理
次に、Promiseを使って複数のリクエストにまとめてタイムアウトを設定する方法を見てみましょう。
ここでは、fetchAllWithTimeoutという関数を定義しています。
この関数は、URLの配列とオプションを受け取り、すべてのリクエストにタイムアウト設定を行ったPromiseを返します。
URLの配列に対して、mapメソッドを使ってfetchのPromiseを作成しています。
そして、Promise.allを使って、すべてのPromiseが完了するのを待ちます。
レスポンスが成功した場合は、それぞれのレスポンスをjsonメソッドで解析し、結果の配列を返します。
一方、いずれかのリクエストがタイムアウトした場合は、AbortErrorが発生し、catchブロックで処理されます。
実行結果は、次のようになります。
- すべてのリクエストが3秒以内に完了した場合
- いずれかのリクエストが3秒以内に完了しなかった場合
○サンプルコード9:Promiseを使った再試行可能なタイムアウト
最後に、Promiseを使ってリクエストが失敗した場合に再試行を行う方法を見てみましょう。
ここでは、fetchWithRetryという関数を定義しています。
この関数は、URLとオプションを受け取り、タイムアウトが発生した場合に指定された回数だけリトライを行うPromiseを返します。
新しいPromiseを作成し、その中でattemptFetchという関数を定義しています。
attemptFetch関数は、AbortControllerを使ってfetchを行い、レスポンスが成功した場合はresolveを呼び出します。
タイムアウトが発生した場合は、retryCountを確認し、残りのリトライ回数があればretryDelayミリ秒待ってからattemptFetchを再帰的に呼び出します。
リトライ回数が0になったら、タイムアウトエラーをrejectします。
実行結果は、次のようになります。
- リクエストが成功した場合
- 1回目のタイムアウト後、2回目のリクエストが成功した場合
- リトライが全て失敗した場合
このように、Promiseを使えば、タイムアウト設定とエラーハンドリングをより細かく制御できます。
再試行やフォールバック処理なども柔軟に実装できるので、実践的なタイムアウト管理に役立つでしょう。
●よくあるエラーと対処法
さて、ここまでfetchAPIでのタイムアウト設定について、様々な方法を見てきましたが、実際にコードを書いていると、エラーに遭遇することがありますよね。
そこで、このセクションでは、よくあるエラーとその対処法について解説していきます。
エラーが発生した時に慌てないように、しっかり理解しておきましょう。
○AbortErrorが発生した場合の対処法
まずは、AbortErrorについて見ていきましょう。
AbortErrorは、リクエストが中止された時に発生するエラーです。
AbortErrorが発生した場合、次のようなことを確認してみてください。
- AbortControllerのシグナルがfetchに正しく渡されているか
- タイムアウト時間が適切に設定されているか
- controller.abort()が意図しないタイミングで呼び出されていないか
また、AbortErrorをキャッチした際は、次のように処理するのが一般的です。
ここでは、errorオブジェクトのnameプロパティを見て、AbortErrorかどうかを判定しています。
AbortErrorの場合は、リクエストが中止されたことをログに出力し、それ以外のエラーの場合は、通常のエラー処理を行っています。
実行結果は、次のようになります。
- リクエストが中止された場合
- それ以外のエラーが発生した場合
○ネットワークエラーとタイムアウトエラーの見分け方
次に、ネットワークエラーとタイムアウトエラーの見分け方について解説します。
ネットワークエラーは、ネットワークの問題やサーバーの障害などが原因で発生します。
一方、タイムアウトエラーは、指定した時間内にレスポンスが返ってこなかった場合に発生します。
これらのエラーを見分けるには、次のようにします。
ここでは、AbortErrorの場合、さらにerror.messageを正規表現でチェックしています。
/^TimeoutError:/は、メッセージが”TimeoutError:”で始まる場合にマッチします。
これにより、タイムアウトエラーとそれ以外のAbortErrorを区別することができます。
実行結果は、次のようになります。
- リクエストがタイムアウトした場合
- リクエストが中止された場合(タイムアウト以外の理由)
- ネットワークエラーが発生した場合
○タイムアウト設定が機能しない場合のデバッグ方法
最後に、タイムアウト設定が機能しない場合のデバッグ方法を見ていきましょう。
タイムアウト設定が効かない場合、次のような点を確認してみてください。
- AbortControllerのシグナルがfetchに正しく渡されているか
- タイムアウト時間が適切に設定されているか
- controller.abort()が正しいタイミングで呼び出されているか
- レスポンスが返ってくるまでの時間が、想定よりも長くなっていないか
特に、最後の点は注意が必要です。
レスポンスが返ってくるまでの時間が長い場合、タイムアウト時間を長めに設定する必要があります。
また、デバッグの際は、次のようにログを出力すると良いでしょう。
ここでは、のようなログを出力しています。
- controller.abort()が呼び出される直前に”Aborting request…”
- レスポンスを受け取った時に”Response received:”
- リクエストが中止された時に”Request aborted”
- リクエストが失敗した時に”Request failed”
これにより、タイムアウト設定がどのように機能しているかを詳細に確認することができます。
実行結果は、次のようになります。
- タイムアウトが発生した場合
- レスポンスが正常に受け取れた場合
- エラーが発生した場合
このように、ログを活用することで、タイムアウト設定の動作を詳細に確認し、問題の原因を特定することができます。
●fetchAPIのタイムアウト設定のベストプラクティス
さて、ここまでfetchAPIでのタイムアウト設定について、様々な方法を見てきましたが、実際のプロジェクトで使うとなると、どのような設定が最適なのか悩むこともあるでしょう。
そこで、ここでは、fetchAPIのタイムアウト設定のベストプラクティスについて解説していきます。
実践で使えるテクニックを身につけて、より堅牢なWebアプリケーションを開発しましょう。
○サンプルコード10:実践で使えるタイムアウト設定のテンプレート
まずは、実践で使えるタイムアウト設定のテンプレートを見てみましょう。
このテンプレートでは、次のような工夫が施されています。
- async/awaitを使って、コードの可読性を向上
- デフォルトのタイムアウト時間を5秒に設定
- fetchOptionsを分離して、fetchのオプションを柔軟に指定可能に
- try…catch…finallyを使って、エラーハンドリングとリソースの解放を適切に実施
- レスポンスのステータスコードをチェックし、適切にエラーハンドリング
実行結果は、次のようになります。
- リクエストが成功した場合
- タイムアウトエラーが発生した場合
- ネットワークエラーが発生した場合
○適切なタイムアウト時間の選び方
次に、適切なタイムアウト時間の選び方について考えてみましょう。
タイムアウト時間を設定する際は、次のような点を考慮する必要があります。
- APIのレスポンス時間の平均値と最大値
- ネットワーク環境の状況
- ユーザーの体感速度への影響
- サーバーリソースの使用効率
一般的には、レスポンス時間の最大値よりも少し長めの時間をタイムアウト時間に設定するのが良いでしょう。
ただし、あまりにも長いタイムアウト時間を設定すると、ユーザーの体感速度が悪くなったり、サーバーリソースを無駄に使ってしまったりする可能性があります。
また、モバイルアプリケーションの場合は、ネットワーク環境の状況によってレスポンス時間が大きく変動することがあるので、柔軟にタイムアウト時間を調整できるようにしておくと良いでしょう。
○エラーハンドリングとログ出力の重要性
タイムアウト設定を行う際は、エラーハンドリングとログ出力も忘れずに実装しましょう。
適切にエラーをハンドリングすることで、予期せぬ動作を防ぎ、アプリケーションの安定性を高めることができます。
また、ログ出力を行うことで、問題が発生した際に原因の特定が容易になります。
次のようなエラーハンドリングとログ出力を行うのがおすすめです。
ここでは、エラーの種類に応じて、適切なログメッセージを出力し、エラー処理を行っています。
- タイムアウトエラーの場合は、”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を使ったタイムアウト処理の例を見てみましょう。
ここでは、fetchイベントをリッスンし、5秒以内にレスポンスが返ってこない場合は、タイムアウトエラーをレスポンスとして返しています。
Service Workerを使った処理は、ブラウザでしか動作しませんが、ネットワークリクエストに対して柔軟な制御ができるので、必要に応じて検討してみると良いでしょう。
まとめ
さて、ここまでfetchAPIでのタイムアウト設定について、基本的な使い方から実践的なテクニックまで、幅広く解説してきました。
AbortSignal.timeoutやAbortControllerを使えば、柔軟かつ堅牢なタイムアウト設定が可能になります。
また、Promiseを活用することで、エラーハンドリングや再試行処理なども実装できます。
適切なタイムアウト時間の選択、丁寧なエラーハンドリング、わかりやすいログ出力などを心がければ、ユーザーに快適な体験を提供できるでしょう。
フロントエンド開発において、ネットワーク通信は避けて通れない課題です。
通信が不安定な環境でも、しっかりとタイムアウト設定を行うことで、アプリケーションの品質を高めることができます。
本記事で紹介した内容を参考に、皆さんの開発するアプリケーションにベストなタイムアウト設定を実装してみてください。
きっと、ユーザーに喜ばれるアプリケーションになるはずです。
fetchAPIでのタイムアウト設定、ぜひマスターしておきましょう!