JavaScriptにおけるAbortControllerを使用したキャンセル可能なFetch操作
Emily Parker
Product Engineer · Leapcell

はじめに
現代のWeb開発の世界では、非同期操作はダイナミックで応答性の高いアプリケーションの礎です。サーバーからのデータ取得から外部リソースの読み込みまで、開発者は予測不可能な時間を要するタスクに常に対処しています。Promiseとasync/awaitはこれらの操作の処理方法を劇的に改善しましたが、一般的な課題が残っています。それは、進行中の非同期タスクが不要になった場合にどうなるかということです。ユーザーがページから離れたり、検索クエリが急速に変化したりして、以前のネットワークリクエストが無効になる可能性があります。これらの操作をキャンセルするメカニズムなしでは、貴重なネットワーク帯域幅を浪費し、不要なサーバーリソースを消費し、アプリケーションで競合状態やメモリリークを引き起こすリスクがあります。この記事では、キャンセル可能な非同期操作の重要な概念、特にfetch APIに焦点を当て、クライアントサイド(ブラウザ)とサーバーサイド(Node.js)の両方のJavaScript環境でこの機能を一貫して実装する方法を説明します。
キャンセル可能なFetchのコアコンセプト
実装の詳細に入る前に、キャンセル可能なfetch操作の達成に不可欠ないくつかのコアコンセプトを明確にしましょう。
非同期操作
本質的に、非同期操作はメインプログラムフローから独立して実行できるタスクであり、プログラムは非同期タスクの完了を待つ間に他のタスクを実行し続けることができます。JavaScriptでは、これは主にイベントループ、Promise、およびasync/await構文を通じて管理されます。fetchは非同期操作の代表的な例です。なぜなら、それは最終的にResponseオブジェクトで解決されるか、エラーで拒否されるPromiseを返すからです。
Fetch API
fetch APIは、ネットワーク経由でリソースを取得するための強力で柔軟なインターフェースを提供します。これはXMLHttpRequestのモダンな代替であり、HTTPリクエストを行うためにより堅牢でPromiseベースのアプローチを提供します。そのシンプルさと強力さが、Web開発者にとっての選択肢となっています。
AbortControllerとAbortSignal
AbortControllerインターフェースは、fetchリクエストをキャンセル可能にするための重要なコンポーネントです。これは、必要に応じて1つ以上のDOMリクエストにシグナルを送り、中止する方法を提供します。AbortControllerインスタンスには、関連付けられたAbortSignalオブジェクトがあります。このAbortSignalは、キャンセルシグナルを監視および反応するために、fetch API(およびその他の非同期API)に渡すことができます。AbortControllerインスタンスでabort()メソッドが呼び出されると、 abortイベントが発行され、その AbortSignal をリッスンしている fetch リクエストはキャンセルされ、そのPromiseは AbortError で拒否されます。
ブラウザでのキャンセル可能なFetchの実装
AbortController APIはモダンブラウザでネイティブにサポートされているため、クライアントサイドでキャンセル可能なfetchリクエストを簡単に実装できます。
ユーザーが離れたり、新しい検索クエリを入力したりするとキャンセルされる可能性のあるユーザーデータを取得する実用的な例で説明しましょう。
// キャンセルメカニズムを備えたユーザーデータ取得関数 async function fetchUserData(userId, signal) { try { const response = await fetch(`https://api.example.com/users/${userId}`, { signal }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); console.log('User data:', data); return data; } catch (error) { if (error.name === 'AbortError') { console.log('Fetch request for user data was aborted.'); } else { console.error('Error fetching user data:', error); } throw error; // 必要に応じてさらに処理するために再スロー } } // 使用例 const controller = new AbortController(); const signal = controller.signal; // ユーザーがフェッチを開始すると想定 const promise = fetchUserData(123, signal); // 2秒後にユーザーが離れるか、新しいリクエストを行うと想定 setTimeout(() => { console.log('Aborting fetch request...'); controller.abort(); }, 2000); promise .then(data => { console.log('Successfully fetched:', data); }) .catch(error => { // AbortErrorはfetchUserData内で処理されますが、他のエラーがここに伝播する可能性があります if (error.name !== 'AbortError') { console.error('Operation failed:', error); } }); // 別の例: 別のユーザーの新しいフェッチリクエスト。古いものをキャンセルする可能性がある // 実際のアプリケーションでは、特定のコンポーネントに対して単一のコントローラーを管理する場合があります // 古いものがキャンセルされるべき場合は、新しいリクエストごとに新しいコントローラーを作成できます。 const searchInput = document.getElementById('search-input'); // そのような要素を想像してください let currentController = null; searchInput?.addEventListener('input', (event) => { if (currentController) { currentController.abort(); // 前のリクエストをキャンセル } currentController = new AbortController(); const newSignal = currentController.signal; const searchTerm = event.target.value; if (searchTerm.length > 2) { // 2文字以上の場合のみ検索 fetchUserData(`search?q=${searchTerm}`, newSignal) .then(results => console.log('Search results:', results)) .catch(error => { if (error.name !== 'AbortError') { console.error('Search failed:', error); } }); } });
このブラウザの例では、AbortControllerを作成し、そのsignalを抽出し、fetch呼び出しに渡します。後でcontroller.abort()が呼び出されると、fetchPromiseはAbortErrorで拒否され、それを特別にキャッチして処理します。このパターンは、予期されるキャンセルと実際のネットワークエラーをフィルタリングするために不可欠です。
Node.jsでのキャンセル可能なFetchの実装
歴史的に、Node.jsにはネイティブなfetch APIがありませんでした。開発者は通常、node-fetchやaxiosなどのサードパーティライブラリに依存していました。Node.js v18以降、fetch APIはグローバルに利用可能で組み込まれており、ブラウザ環境との整合性が大幅に向上しました。特に重要なのは、この組み込みfetch APIも AbortController をサポートしていることです。
以前の例をNode.js環境で適応させてみましょう。
// Node.js v18以降では、Node.js固有のグローバルfetchが利用可能です。 // 古いバージョンを使用している場合は、`npm install node-fetch`を実行し、`import fetch from 'node-fetch';` // そして、AbortControllerが利用可能であることを確認する必要があります(例:`abort-controller` npmパッケージから)。 // キャンセルメカニズムを備えたユーザーデータ取得関数 async function fetchUserDataNode(userId, signal) { try { const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, { signal }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); console.log('User data from Node.js:', data); return data; } catch (error) { if (error.name === 'AbortError') { console.log('Node.js fetch request for user data was aborted.'); } else { console.error('Error fetching user data from Node.js:', error); } throw error; } } // Node.jsでの使用例 const controllerNode = new AbortController(); const signalNode = controllerNode.signal; console.log('Initiating Node.js fetch...'); const promiseNode = fetchUserDataNode(1, signalNode); // Node.jsでのクリーンアップまたはタイムアウトをシミュレート setTimeout(() => { console.log('Aborting Node.js fetch request...'); controllerNode.abort(); }, 1500); // 1.5秒後に中止 promiseNode .then(data => { console.log('Node.js successfully fetched:', data); }) .catch(error => { if (error.name !== 'AbortError') { console.error('Node.js operation failed:', error); } }); // 別の例: 中止される前に完了するリクエスト const controllerWillComplete = new AbortController(); const signalWillComplete = controllerWillComplete.signal; console.log('Initiating a fetch that will complete...'); fetchUserDataNode(2, signalWillComplete) .then(data => console.log('Node.js fetch completed successfully for user 2:', data)) .catch(error => { if (error.name !== 'AbortError') { console.error('Node.js fetch failed for user 2:', error); } }); // controllerWillComplete の中止呼び出しがないため、完了するはずです。
Node.jsのコード構造はブラウザのものと似ています。AbortControllerとAbortSignalの動作はまったく同じであり、両方の環境でキャンセル可能なfetch操作への一貫したアプローチを可能にします。このクロス環境の一貫性は大きな利点であり、フルスタックJavaScriptアプリケーションで作業する開発者の認知負荷を軽減し、開発を簡素化します。
アプリケーションシナリオ
キャンセル可能なfetch操作は、さまざまな実世界のシナリオで非常に役立ちます。
- 検索オートコンプリート/タイプアヘッド: ユーザーが急速に入力すると、以前の検索リクエストは冗長になります。それらをキャンセルすることで、不要なネットワークトラフィックが防止され、最新の関連結果のみが表示されるようになり、古い、遅い応答が新しい応答を上書きする競合状態を防ぎます。
 - ユーザーナビゲーション: ユーザーが
fetchリクエストを開始したページまたはコンポーネントから離れる場合、進行中のリクエストをキャンセルすることで、リソースが解放され、潜在的なバックグラウンドプロセスがクリーンアップされます。 - ポーリング/ロングポーリングのクリーンアップ: 定期的にデータを取得するメカニズムを実装する場合、次のポーリングをスケジュールする前に現在のポーリングをキャンセルすること(例:コンポーネントがアンマウントされるとき)は、リソース管理に不可欠です。
 - 条件付きデータ読み込み: 複数のタブがあるダッシュボードを想像してください。ユーザーがタブを切り替えると、新しいタブのデータ読み込みを優先するために、以前アクティブだったタブに関連するフェッチをキャンセルしたい場合があります。
 - タイムアウトと再試行: 
AbortControllerは主に明示的なキャンセルをシグナルしますが、タイムアウトメカニズムと組み合わせることができます。たとえば、fetchに時間がかかりすぎる場合、自動的に中止することができます。 
結論
キャンセル可能な非同期操作、特にfetch APIを使用した実装は、堅牢で効率的でユーザーフレンドリーなWebアプリケーションを構築するための重要な技術です。AbortControllerとAbortSignalパターンを活用することで、開発者はネットワークリクエストを効果的に管理し、リソースの無駄を防ぎ、ブラウザとNode.jsの両方の環境でアプリケーションの応答性を向上させることができます。この一貫したアプローチは、開発者が非同期タスクのダイナミックな性質を適切に処理する、よりクリーンで保守性の高いコードを書くことを可能にします。進行中の操作をキャンセルできる機能は、単なる最適化ではなく、モダンな非同期プログラミングの基本的な側面です。