Async/Awaitのマスター: 高度なエラー処理とリトライパターン

Async/awaitは非同期JavaScriptを変革しましたが、適切なエラー処理は依然として難しい課題です。本記事では、リトライロジック、タイムアウト処理、フォールバック戦略、エラー回復パターンを備えた堅牢な実装を分析し、実運用向けアプリケーションに不可欠な手法を紹介します。

2025年12月17日 読了時間: 21分
Async/Awaitのマスター: 高度なエラー処理とリトライパターン

はじめに: 基本的なAsync/Awaitを超えて

async/awaitの構文により非同期コードは読みやすくなりますが、実運用アプリケーションでは高度なエラー処理、リトライ機構、タイムアウト、フォールバック戦略が必要です。ネットワーク障害、API制限、一時的なエラーに対して、失敗を優雅に処理できる堅牢なコードが求められます。ここでは、実運用の課題に対応する包括的なasync/await実装を分析します。

元のコード: 一般的な落とし穴

初期の実装には、実運用で問題を引き起こす可能性のある重要な問題がいくつかありました。修正版を見て、何が問題だったのかを理解しましょう。

修正版コード: レジリエントなAPIクライアント

class APIClient { ... } // 簡潔のためコードは省略

修正された重要なエラー

元のコードにはいくつかの重大なバグがありました。まず、AbortControllerがtryブロック内で作成されていましたが、catch内で適切にクリーンアップできるようアクセス可能である必要がありました。次に、isRetryableErrorメソッドがnull/undefinedのパラメータを受け取り、ランタイムエラーを引き起こす可能性がありました — nullチェックを追加して修正。さらに、AbortErrorが他のエラーと区別されず、手動キャンセル時に無限リトライが発生する可能性がありました。最後に、handleFailureでerror.messageにアクセスする際にオプショナルチェイニングを使用しておらず、errorがundefinedの場合にクラッシュするリスクがありました。

タイムアウトの実装にPromise.raceを使用

コードはPromise.raceを使用して、タイムアウト機能を優雅に実装しています。これは実際のfetchリクエストとタイムアウト用のPromiseを競わせます。どちらが先に解決または拒否されるかで結果が決まります。タイムアウトが発動した場合、controller.abort()を呼び出してfetchをキャンセルし、帯域とメモリの無駄を防ぎます。このパターンは、単にfetchをタイムアウトでラップするよりも優れており、リクエストを無視するのではなく、実際にキャンセルします。

ジッター付き指数バックオフ

calculateBackoffメソッドは指数バックオフを実装しています — 各リトライは順次長く待機します(1秒、2秒、4秒、8秒)。これにより、負荷がかかっているサーバーを圧倒しません。さらにジッター(ランダム遅延)を加えることで、複数クライアントが同時にリトライして連鎖的な障害を引き起こす「サンダリングハード」問題を防ぎます。この組み合わせは、分散システムでのリトライロジックの業界標準であり、AWS、Google Cloud、主要APIでも使用されています。

安全チェック付きインテリジェントなリトライロジック

改良されたisRetryableErrorメソッドには必須の安全チェックが含まれています。まず少なくとも1つのパラメータが提供されているかを確認し、null参照エラーを防ぎます。AbortErrorを明示的にチェックしfalseを返します — 手動キャンセルはリトライされるべきではありません。このメソッドは、リトライすべき一時的エラー(ネットワーク障害、タイムアウト、5xxサーバーエラー、429制限)と、恒久的な失敗(400 Bad Request、404 Not Found、401 Unauthorized)を区別します。実運用システムにとって、このインテリジェントな判断は重要です。

エラーコンテキストの強化

修正版コードでは、エラー応答のボディを解析し、失敗時により多くのコンテキストを提供します。responseオブジェクトと解析済みボディを投げられたエラーに添付し、呼び出し側のコードが詳細なエラー情報にアクセスできるようにしています。オプショナルチェイニング(?.)の使用により、undefinedプロパティへのアクセス時のクラッシュを防ぎます。この防御的プログラミングアプローチにより、予期しないAPIレスポンスがあってもクライアントは安定して動作します。

構造化されたエラー処理

単にエラーを投げるのではなく、この実装では成功フラグ、データ、エラーメッセージ、試行回数などのメタデータを含む構造化されたレスポンスオブジェクトを返します。このアプローチにより、呼び出し側のコードはユーザー向けのメッセージ表示、エラーの適切なログ記録、代替ワークフローの実行など、情報に基づいた意思決定を行えます。成功・失敗に関わらず一貫した返却フォーマットは、アプリケーション全体のエラー処理を簡素化します。

安全なフォールバック戦略

すべてのリトライが失敗した場合、handleFailureは適切なnull処理を行った上で優雅な劣化戦略を実装します。オプショナルチェイニングにより、errorがundefinedでもコードはクラッシュしません。もしフォールバックデータが存在すれば(以前の成功リクエストやデフォルト値など)、完全な失敗の代わりにそれを返します。これにより、APIが完全に停止している場合でもアプリケーションは動作を維持できます。fromCacheフラグは、データが古くなっている可能性をUIに示すことができます。

リトライのforループパターン

リトライロジックにcontinueを使ったforループを使用することは、再帰やwhileループよりも明確で読みやすいです。最大リトライ回数を明示的にし、スタックオーバーフローの問題を防ぎ、処理の流れを分かりやすくします。試行カウンターは進行状況の追跡やログに役立ちます。非リトライ可能なエラーに遭遇した際にループを抜けることで、無駄な遅延を防ぎます。このパターンは他の手法よりも可読性と保守性に優れています。

AbortControllerのスコープ管理

修正版コードでは、AbortControllerをループレベルで宣言し、各イテレーション内でアクセス可能にしています。これにより、タイムアウトやエラー時に適切なキャンセルが可能です。各試行ごとに新しいAbortControllerを作成することで、リクエストキャンセルを精密に制御できます。最新のfetchとAbortControllerは、JavaScriptにおけるキャンセル可能な非同期操作の標準であり、古いパターン(Promiseキャンセルトークンなど)に代わります。

HTTPメソッドの完全サポート

修正版の実装では、GETとPOSTに加え、PUTとDELETEメソッドも追加し、CRUD操作を完全にサポートします。各メソッドはリクエストタイプを適切に設定し、必要に応じてボディをJSONにシリアライズします。これにより、現実のAPIインタラクションで必要なすべてのHTTPメソッドに対応できる柔軟なクライアントとなります。

使用例の改善

デモ関数は、視覚的インジケーター(✓、⚠、ℹ、✗)を使用して、コンソール出力をより分かりやすくしました。fromCacheフラグも適切に処理し、データが古い可能性がある場合にユーザーに通知します。エラー処理は、優雅に劣化したレスポンスと予期せぬエラーを区別し、それぞれに適切なフィードバックを提供します。

本番環境での考慮点

本番環境では、連鎖的な障害を防ぐサーキットブレーカーパターン、APIクォータを尊重するレートリミティング、冗長な呼び出しを避けるリクエスト重複排除、異なる失敗モードに対応するエラー型の定義などを追加する必要があります。監視ツールとの統合により、リトライ率、失敗パターン、パフォーマンス指標を追跡できます。認証トークンのリフレッシュロジックもこの層に含まれることが多いです。axiosのインターセプターやkyのような信頼性の高いライブラリの使用を検討してください。

重要ポイント

  • エラー確認関数では常にパラメータを検証して、null参照エラーを防ぐ。
  • 未定義の可能性があるプロパティにアクセスする際はオプショナルチェイニング(?.)を使用してクラッシュを防ぐ。
  • 適切なスコープでAbortControllerを宣言して、クリーンアップ時にアクセス可能にする。
  • AbortErrorを他のエラーと区別する — 手動キャンセルは決してリトライしない。
  • 投げられたエラーに詳細なエラーコンテキスト(response、body)を添付してデバッグを容易にする。
  • Promise.raceを使用してfetchとタイムアウトPromiseを競わせ、タイムアウトを優雅に実装する。
  • ジッター付き指数バックオフにより、サーバー過負荷やサンダリングハード問題を防ぐ。
  • 成功フラグを持つ構造化レスポンスオブジェクトにより、一貫した情報提供が可能。
  • フォールバック戦略により、API完全停止時でも優雅な劣化が可能。
  • forループは再帰よりも安全で明確なリトライロジックを提供。
  • HTTPメソッドの完全サポート(GET、POST、PUT、DELETE)によりクライアントの柔軟性が向上。
  • nullチェックとオプショナルチェイニングを組み合わせた防御的プログラミングにより、予期しないクラッシュを防止。

タグ:

#JavaScript#Async/Await#エラー処理#リトライロジック#APIクライアント#Promises#ネットワーク耐障害性#本番コード#バグ修正#2025#コード分析

共有: