プロンプトとコンテキスト
検索ページでは、サジェスト、プロフィールデータ、検索結果を並行してリクエストします。新しいクエリや画面遷移が発生した場合は古い処理を速やかにキャンセルすべきであり、ネットワークリクエストが停滞した場合はタイムアウトさせる必要がありますが、バックグラウンドから復帰した際にフリーズしていた時間をオンラインのリクエスト時間としてカウントしてはなりません。AbortSignal.timeout()、AbortSignal.any()、および AbortController を使用してキャンセル処理を設計してください。
これは、フロントエンド、Web パフォーマンス、および非同期アーキテクチャの面接に適したテーマです。すべての例外を一律に「リクエストがタイムアウトしました」と表示するのではなく、ユーザーによる中止、タイムアウト、ページのライフサイクル、および実際のネットワーク障害を分離することが重要です。
面接官がテストしていること
優れた回答では、AbortSignal.timeout() が TimeoutError で自動的に中止されるシグナルを返し、ユーザーまたはコントローラーによる中止は通常 AbortError になることを説明します。また、アクティブ時間のセマンティクスについても説明します。ページが bfcache にある間や Worker が一時停止している間、タイマーは一時停止します。複数のシグナルは、診断可能な理由を保持したまま AbortSignal.any() で組み合わせることができます。
最初に尋ねるべき確認事項
- どのリクエストが安全にキャンセル可能で、どの書き込み処理は完了させるか、あるいは後で状態を問い合わせる必要があるか?
- タイムアウトは、ユーザーのアクション、リクエストのディスパッチ、またはページの可視性のどこから開始されるか?
- ユーザーによる中止、アンマウント、画面遷移、タイムアウトで、それぞれ異なるメッセージングが必要か?
- 対象ブラウザはこれらの静的メソッドをサポートしているか、またフォールバックでは手動タイマーを維持すべきか?
- リトライ、重複排除、キャッシング、および結果の競合状態はどのように処理されるか?
30秒の回答フレームワーク
「各リクエストについて、ユーザーによるキャンセルと AbortSignal.timeout() を組み合わせ、TimeoutError、AbortError、およびネットワークエラーを個別に分類します。タイムアウトはアクティブ時間を使用するため、一時停止や bfcache がオンラインのレイテンシとして報告されることはありません。新しいクエリは古いコントローラーをキャンセルし、結果の反映時にもリクエストのシーケンスまたはシグナルをチェックして、古いデータが優先されないようにします。古いブラウザにはコントローラーとタイマーによるフォールバックを提供し、キャンセル、タイムアウト、再開、および競合についてテストします。」
ステップごとの詳細な回答
ステップ 1: 3つのキャンセル元を分離する
AbortController.abort() はアプリケーション起点です。AbortSignal.timeout(ms) はアクティブ時間が上限に達したときに中止します。AbortSignal.any([...]) はいずれかの入力シグナルが中止されたときに発火します。すべて fetch に渡すことができますが、ページ離脱がサービス障害としてログに記録されないように、ビジネス層でその理由を保持する必要があります。
const controller = new AbortController();
const timeout = AbortSignal.timeout(5000);
const signal = AbortSignal.any([controller.signal, timeout]);
fetch(url, { signal });ステップ 2: 理由によって分類する
タイムアウトは TimeoutError DOMException を使用し、ユーザーによるキャンセルやブラウザの停止は通常 AbortError を使用します。DNS、接続リセット、および CORS の問題は他のエラーとして現れる場合があります。例外をキャッチするコードは、メッセージング、リトライ、およびログの重要度を選択する前に、signal.reason または例外名を調べる必要があります。
ステップ 3: アクティブ時間を理解する
timeout() は、単純な実時間(ウォールクロック)ではなくアクティブ時間を測定します。仕様では、ページが bfcache にある間や Worker が一時停止している間、タイマーが一時停止することが示されています。これは、サーバーの絶対的なデッドラインではなく、ユーザーが体感するアクティブなリクエストウィンドウに適しています。サーバーには依然として独自のタイムアウトと冪等性ポリシーが必要です。
ステップ 4: 検索の競合状態を処理する
ユーザーが連続して入力した場合は、新しいクエリ用のシグナルを作成する前に古いコントローラーを中止します。古いネットワーク処理が完了していたとしても、そのコールバックがまだキューに残っている可能性があります。状態をコミットする前に、リクエストシーケンス、現在のクエリ、または signal.aborted を確認してください。キャンセル処理だけでは結果のバージョン順序は保証されません。
ステップ 5: キャンセル可能な読み取りと書き込みを分離する
検索、画像、サジェストの読み取りは通常キャンセル可能です。決済、注文、アップロードの書き込みは、サーバー上ですでに有効になっている可能性があります。クライアントの待機をキャンセルしても、サーバー側の副作用は元に戻りません。単に短いタイムアウトを設定するのではなく、書き込みには冪等性キー、ステータス問い合わせ、またはバックグラウンドタスクを使用してください。
ステップ 6: シグナルのライフタイムを設計する
アンマウント時にコンポーネントのコントローラーを中止し、画面遷移時にページレベルのシグナルを再利用し、個別の操作に対してリクエストタイムアウトを追加します。恒久的に中止されたグローバルシグナルを以降のリクエストと共有してはなりません。操作ごとに新しいシグナルを作成してください。診断が重要な場合は、異なるソースに明示的な理由を付与します。
ステップ 7: 互換性フォールバックを提供する
古いブラウザに AbortSignal.timeout または any がない場合は、AbortController と setTimeout を組み合わせ、タイマーをクリアして理由を正規化します。機能検出はランタイムで行うべきであり、デフォルトのパスは静的メソッドのない環境も処理できなければなりません。
ステップ 8: ライフサイクルとクリーンアップを検証する
素早い連続入力、アンマウント、画面遷移、バックグラウンド一時停止、bfcache からの復帰、タイムアウト、ユーザーによる中止、ネットワーク障害、および繰り返しのリトライをテストします。リクエストがキャンセルされ、タイマーがクリアされ、古い結果がコミットされず、メッセージが正確であり、例外のキャッチによって未処理の Promise や状態更新の警告が発生しないことを確認します。
トレードオフと境界
マージされたシグナルはキャンセル処理のグルーコードを削減しますが、サーバーのトランザクションを可逆にするわけではなく、レスポンスが絶対に届かないことを保証するものでもありません。アクティブ時間のセマンティクスはユーザー体験には適していますが、エンドツーエンドの SLA には適していません。サーバーのデッドラインと連携しながら、リクエストの種類、ネットワーク状態、およびリトライバジェットに応じてタイムアウト値を設定してください。
Promise.race だけを使用してタイムアウトをシミュレートし、基盤となるリクエストを実行したままにしてはなりません。ネットワークとリソースを停止できるように AbortSignal を渡してください。すべての中止をエラーとしてログに記録しないでください。通常の画面遷移によってアラートが汚染されてしまいます。
ロールアウト計画と検証証跡
検索の読み取りリクエスト用のステートマシンから開始します。シグナルの作成、ディスパッチ、理由の分類、バージョン管理された結果のコミット、アンマウント時の中止を行います。機密性の高いクエリテキストを除外し、リクエストタイプ、理由、アクティブ時間、リトライ回数、および最終状態を記録します。
bfcache、Worker、低速ネットワーク、高速入力にわたって、サポートされているブラウザとフォールバックブラウザを比較します。合格基準には、古い結果の上書きがないこと、タイマーのリークがないこと、正確なキャンセルメッセージ、偶発的な書き込み中止がないこと、およびサーバーの冪等性またはステータス問い合わせの証跡が含まれます。
よくある落とし穴とフォローアップ
TimeoutError と AbortError の混同
ユーザーによる中止、アンマウント、タイムアウトでは、次のステップが異なります。理由によって分類し、メッセージング、ログ記録、およびリトライを選択してください。
クライアントの中止がサーバーの書き込みをロールバックすると想定すること
リクエストはすでにサーバーに到達している可能性があります。書き込みには冪等性キー、ステータス問い合わせ、または補償フローが必要です。
実際のキャンセルを Promise.race で置き換えること
Race を使用すると呼び出し元が待機する対象は変わりますが、基盤となる fetch は停止しません。AbortSignal を渡し、リソースをクリーンアップしてください。
bfcache 周辺のアクティブ時間を無視すること
ページが一時停止している間、タイムアウトは一時停止することがあります。復帰後の時間をオンラインリクエストの SLA として扱わないでください。
古い結果が優先されるのをどのように防ぐか?
古いコントローラーを中止するだけでは不十分です。レスポンスの順序によって状態が変わらないように、コミットする前にクエリのシーケンス、バージョン、または現在のクエリを比較してください。