TypeScriptでPromiseを中断!AbortControllerで非同期処理をキャンセルする方法
生徒
「TypeScriptで非同期処理を始めた後で、やっぱり止めたくなったときはどうすればいいですか?」
先生
「非同期処理を途中でキャンセルしたいときは、AbortControllerという便利な仕組みを使うのが一般的ですよ。」
生徒
「アボートコントローラー?なんだか難しそうですが、初心者でも使えますか?」
先生
「大丈夫です。注文をキャンセルする店員さんのような役割だと考えれば簡単ですよ。具体的な使い方を一緒に学んでいきましょう!」
1. 非同期処理とキャンセルの必要性
プログラミングにおける非同期処理とは、重たい作業を裏側で実行しながら、他の作業を止めずに進める仕組みのことです。TypeScriptでは主にPromise(プロミス)やasync/await(エイシンク・アウェイト)を使ってこれを表現します。
しかし、一度始めた処理が不要になる場面があります。例えば、インターネットから大きな画像をダウンロードしている途中でユーザーが別のページに移動してしまった場合、そのままダウンロードを続けるのは通信費やメモリの無駄遣いになってしまいます。このように「もうこの結果はいらないよ」と伝えるのがキャンセル処理です。
2. AbortControllerとは何か
AbortController(アボートコントローラー)は、非同期処理を外側から安全に中断させるための標準的な道具です。直訳すると「中断を管理する人」という意味になります。
この仕組みは、主に二つのパーツで動いています。
- コントローラー本体:中断の命令を出すボタンのような役割です。
- シグナル(Signal):中断の命令を監視している通信機のような役割です。
非同期処理を行う関数に、あらかじめこの「シグナル」を渡しておきます。そして、好きなタイミングでコントローラーの「中断ボタン」を押すと、シグナルを通じて処理を止めることができるのです。
3. 基本的な使い方とコードの書き方
まずは、最もシンプルなキャンセル処理の書き方を見てみましょう。ここでは、特別なタイマー処理を作成し、途中で止める例を紹介します。
// 1. コントローラーを作成する
const controller = new AbortController();
// 2. 監視用のシグナルを取り出す
const signal = controller.signal;
// 非同期処理の定義
const myTask = async (s: AbortSignal) => {
try {
console.log("処理を開始します...");
// 中断を検知するためのリスナーを設定
if (s.aborted) {
throw new Error("開始前にキャンセルされました");
}
// 3秒待つシミュレーション(実際はここで通信などを行う)
await new Promise((resolve, reject) => {
const timer = setTimeout(() => {
console.log("処理が完了しました!");
resolve("成功");
}, 3000);
// シグナルが中断を検知したらタイマーを止める
s.addEventListener("abort", () => {
clearTimeout(timer);
reject(new Error("中断されました"));
});
});
} catch (err) {
console.log("エラー発生:", err.message);
}
};
// 実行
myTask(signal);
// 1秒後にキャンセルを実行してみる
setTimeout(() => {
console.log("キャンセルボタンが押されました!");
controller.abort();
}, 1000);
実行結果は以下のようになります。
処理を開始します...
キャンセルボタンが押されました!
エラー発生: 中断されました
4. fetch関数での実戦的なキャンセル
最もよく使われるのは、インターネットからデータを取得するfetch(フェッチ)関数での利用です。fetchは標準でAbortSignalを受け取れるようになっているため、自分で複雑なリスナーを書く必要がありません。
インターネットの通信は、相手のサーバーが重かったり、自分の電波が悪かったりすると非常に時間がかかります。ユーザーが待てなくなって別の操作をしたときに、この通信をピタッと止めることができます。
async function downloadData() {
const controller = new AbortController();
// 5秒経ったら自動でキャンセルする設定
setTimeout(() => {
controller.abort();
}, 5000);
try {
console.log("データの取得を試みます...");
const response = await fetch("https://example.com/api/data", {
signal: controller.signal // ここにシグナルを渡すだけ!
});
const data = await response.json();
console.log("取得成功:", data);
} catch (error: any) {
if (error.name === "AbortError") {
console.log("通信がタイムアウト、またはキャンセルされました。");
} else {
console.log("その他の通信エラー:", error.message);
}
}
}
downloadData();
このように、オプションの中にsignalを含めるだけで、簡単に連動させることができます。error.nameを確認することで、それが「意図的なキャンセル」だったのか「故障などのエラー」だったのかを見分けることができます。
5. AbortSignalの便利なプロパティ
AbortSignal(アボートシグナル)には、今の状態を確認するための便利な機能がいくつか備わっています。これらを使いこなすことで、より丁寧なプログラムを書くことができます。
- aborted:現在すでにキャンセルされているかどうかを
true(真)かfalse(偽)で返します。 - reason:なぜキャンセルされたのか、その理由を保持することができます。
例えば、キャンセルの理由として「ユーザーが閉じた」「タイムアウトした」などの情報を付与して、後から原因を特定しやすくすることも可能です。
const controller = new AbortController();
// 理由を添えてキャンセル
controller.abort("ユーザーが戻るボタンを押しました");
if (controller.signal.aborted) {
console.log("状態: 中断済み");
console.log("理由:", controller.signal.reason);
}
状態: 中断済み
理由: ユーザーが戻るボタンを押しました
6. なぜキャンセルが必要なのか?
初心者の方の中には、「放置しておけばそのうち終わるのだから、わざわざキャンセルしなくてもいいのでは?」と思う方もいるかもしれません。しかし、大規模なアプリになればなるほど、この管理が重要になります。
まず第一にメモリの節約です。終わるはずのない処理がバックグラウンドで動き続けると、パソコンやスマートフォンの動作がどんどん重くなってしまいます。第二に誤動作の防止です。前のページで行っていた古い処理の結果が、今のページに突然表示されてしまうようなバグを防ぐことができます。
プログラミングの世界では、自分が始めた後始末をしっかり行うことが、一人前のエンジニアへの第一歩です。
7. 複数の処理をまとめてキャンセルする
AbortControllerの面白いところは、一つのシグナルを複数の非同期処理に使い回せる点です。例えば、三つのデータを同時に取得し始めたとして、どれか一つでも失敗したり、途中で止めたくなったりしたときに、一つの命令で全てを停止できます。
const controller = new AbortController();
const signal = controller.signal;
const taskA = fetch("/api/user", { signal });
const taskB = fetch("/api/posts", { signal });
const taskC = fetch("/api/settings", { signal });
// 何らかの理由で全て中断
function stopAllTasks() {
console.log("全ての通信を遮断します。");
controller.abort();
}
// 三つの通信をまとめて監視
Promise.all([taskA, taskB, taskC])
.then(() => console.log("全て完了"))
.catch((err) => console.log("一括キャンセルの検知:", err.name));
このように、親玉となるコントローラーが一つあれば、それに紐付いた子分たちの動きを統制することができるのです。これは、非常に効率的で読みやすいコードの書き方と言えます。
8. タイムアウト処理を簡単に作る方法
最新のJavaScript環境やTypeScriptでは、AbortSignal.timeout()というさらに便利な方法も登場しています。これは、指定した時間が経過すると自動的に「中断シグナル」を発動してくれる特殊なシグナルです。
async function fastFetch() {
try {
// 2秒で自動的にキャンセルされるシグナルを作成
const signal = AbortSignal.timeout(2000);
const response = await fetch("https://example.com/slow-api", { signal });
const result = await response.json();
console.log(result);
} catch (err: any) {
if (err.name === "TimeoutError") {
console.log("2秒以内に終わらなかったので諦めました");
} else {
console.log("その他のエラー:", err);
}
}
}
わざわざnew AbortController()を自分で作らなくても、時間制限だけが目的ならこの一行で解決してしまいます。どんどん便利になっていくTypeScriptの機能を活用しましょう。
9. 初心者がハマりやすいポイント
キャンセル処理を実装する際に、いくつか気をつけるべき点があります。まず、一度abort()を呼んでしまったコントローラーは再利用できません。もう一度同じようなキャンセル可能な処理を行いたい場合は、新しくコントローラーを作り直す必要があります。
また、try...catch構文を使ってエラーを捕まえることも忘れないでください。キャンセルされたとき、プログラムは「中断されたよ!」というエラーを投げます。これを適切に処理しないと、アプリ全体が真っ白になったり、止まってしまったりすることがあります。キャンセルのエラーは「悪いこと」ではなく「予定通りの停止」として扱ってあげましょう。