【ITニュース解説】AbortSignal.timeout() and AbortSignal.any(): Cancel Any Async Call in TypeScript
2026年10月24日に「Dev.to」が公開したITニュース「AbortSignal.timeout() and AbortSignal.any(): Cancel Any Async Call in TypeScript」について初心者にもわかりやすく解説しています。
ITニュース概要
非同期処理のタイムアウトで、従来の`setTimeout`は待つのをやめるだけで、裏の通信等は継続しリソースを無駄にしていた。`AbortSignal.timeout()`は指定時間後に、`AbortSignal.any()`は複数の条件で、非同期処理自体を確実にキャンセルする。これにより、無駄なリソース消費を防ぎ、堅牢なプログラムが書ける。
ITニュース解説
ITシステム開発では、外部のサービスやデータとやり取りする非同期処理が頻繁に利用される。ウェブサイトからデータを取得する fetch のような処理がその代表例だが、もし相手からの応答が遅い、あるいは全く来ないといった状況になると、いつまでも処理が待ち続けてしまい、アプリケーション全体のパフォーマンスに悪影響を与えたり、システムが不必要にリソースを消費し続けたりする問題が発生する。このため、処理に「制限時間」(タイムアウト)を設けることは非常に重要だ。
しかし、これまでのタイムアウト処理の実装方法には限界があった。一般的な方法として、setTimeoutという関数を使って一定時間後にエラーを発生させる処理と、Promise.raceという複数の非同期処理のうち最も早く完了したものを採用する仕組みを組み合わせることがよく行われていた。この方法は、あくまで「呼び出し元が結果を待つのをやめる」というだけで、裏で実際に動いている「仕事」(例えば、ダウンロード中の大きなファイル)自体を中断させることはできなかった。結果として、ダウンロードは水面下で続き、ネットワーク接続やメモリなどのリソースを無駄に占有し続ける「リソースリーク」が発生してしまうことが課題だった。これにより、たとえエラーが表示されても、システムはまだ不要な処理を続けているという非効率な状態に陥ることがあったのだ。
このような問題を解決するために、JavaScriptの実行環境(Node.js、ブラウザなど)に新しく導入されたのが、AbortSignal.timeout()という機能だ。これは、指定した時間が経過すると自動的に「アボート」(中止)されるシグナル(信号)を生成する。このシグナルを fetch のような非同期処理を扱う関数に渡すことで、時間切れになった際に、その関数が実行している実際の処理を中断させることが可能になった。例えば、fetchにAbortSignal.timeout(2000)(2秒でタイムアウト)を渡せば、2秒経っても応答がない場合に、ネットワーク接続そのものを切断し、ダウンロード処理も中止される。これにより、不要なリソースの消費を防ぎ、システムをよりクリーンな状態に保ちながら非同期処理を終了させられるようになった。ただし、AbortSignal.timeout()は呼び出された時点から時間計測を開始するため、利用する非同期処理ごとに新しく生成する必要がある点に注意が必要だ。一度生成したシグナルを使い回すと、時間が経過した後に呼び出した処理はすぐにタイムアウトしてしまう可能性がある。
実際のアプリケーションでは、タイムアウトだけでなく、複数の理由で処理を停止させたい場合がよくある。例えば、「ユーザーが画面のキャンセルボタンを押した」「システム全体がシャットダウンする」といった、アプリケーションの状況に応じた停止だ。AbortSignal.any()は、このような複数の停止条件を柔軟に組み合わせるための強力な機能である。これは、複数のアボートシグナルをリストとして受け取り、その中のどれか一つでもアボートされたときに自身もアボートする新しいシグナルを生成する。これにより、ユーザーからのキャンセルシグナルと、時間によるタイムアウトシグナルの両方を監視する統合されたシグナルを一つにまとめることが可能になる。例えば、ウェブページからデータを取得する際に、ユーザーが途中で操作をキャンセルすることも、ネットワークが遅くて10秒経ったら自動でタイムアウトさせることも、一つのシグナルで制御できるのだ。この機能は、エラー発生時に処理を複数回試行する「リトライ処理」と組み合わせるとさらに強力だ。各リトライ試行ごとに短いタイムアウトを設定しつつ、全体のリトライ期間にも上限を設ける。そして、これら全てのタイムアウト条件とユーザーのキャンセルシグナルをAbortSignal.any()で統合することで、どのタイミングでも適切な処理中断を実現できる。どのシグナルが原因でアボートされたかという情報も、後から確認できるため、詳細なログ出力やユーザーへのフィードバックに役立つ。
fetchのような組み込みの非同期関数だけでなく、自分で作成したループ処理や複数のステップからなる非同期関数でも、このアボートシグナルを活用することが重要だ。signal.throwIfAborted()というメソッドを使うと、そのシグナルが既にアボートされていた場合に、その場でエラーを発生させて処理を中断できる。自作の非同期関数では、このシグナルを引数として受け取り、ループの各イテレーションの開始時や、次の大きな処理ステップに移る前にsignal.throwIfAborted()を呼び出すことで、協調的な停止を実現できる。また、その関数が内部で別の非同期関数を呼び出す場合も、アボートシグナルを渡すように徹底することが大切だ。これにより、アプリケーション全体の非同期処理が、どの層からでも適切に停止できるようになる。
非同期処理がアボートされた際、プログラムは通常、try-catchブロックでエラーを捕捉する。しかし、JavaScriptのエラーハンドリングでは、捕捉したエラーが具体的に何が原因で発生したのか(タイムアウトなのか、ユーザーがキャンセルしたのか、あるいは予期せぬネットワークエラーなのかなど)を正確に判断するのが難しい場合がある。特にTypeScriptでは、catch句の変数がunknown型として扱われるため、エラーオブジェクトのプロパティに直接アクセスできない。このような状況でも、アボートシグナルが持っているsignal.reasonというプロパティを参照することで、実際に何が原因でアボートされたのかを正確に知ることができる。このreasonプロパティは、シグナルがアボートされたときの詳細な情報(例えば、タイムアウトの場合はTimeoutErrorという名前のDOMException)を保持している。そのため、エラーの種類を特定するためのヘルパー関数を作成し、まずアボートシグナルの状態を確認し、もしシグナルがアボートされていればそのreasonを信頼し、そうでなければ発生したエラー自体の情報を見る、というアプローチが推奨される。これにより、エラーの種類に応じてユーザーに適切なメッセージを表示したり、再試行するかどうかを判断したりといった、より洗練されたエラーハンドリングが可能になる。
これらの新しい機能は非常に便利だが、いくつか注意すべき点もある。AbortSignal.any()で生成されたシグナルにイベントリスナーを追加した場合、そのシグナルがアボートされても自動的にリスナーが解除されるわけではないため、メモリリークを防ぐためにも、処理が完了したら必ずremoveEventListenerでリスナーを解除する必要がある。また、シグナルを受け入れないように設計された古いライブラリや、自作コード内でシグナルを適切に伝播させていない部分では、アボートシグナルを受け取っても処理が停止しない。そのため、全ての非同期処理の層でアボートシグナルの概念を意識し、シグナルを引数として渡し、適切にチェックを行うようにコードを記述することが重要となる。
AbortSignal.timeout()やAbortSignal.any()といった新しいAPIの登場は、TypeScriptを含むモダンなJavaScript開発において、非同期処理の信頼性と効率性を大幅に向上させる。これらの機能を活用することで、システムは不必要な処理を止め、リソースを効率的に利用し、ユーザー体験を改善できる。