キャンセル処理(AbortController)を一言でいうと
AbortController は、
「一度投げた非同期処理(特に fetch)を、あとから“やっぱりやめる”と止めるための仕組み」 です。
普通の Promise や async/await には「キャンセル」という概念がありません。
一度 fetch を投げたら、結果が返るかエラーになるまで基本的には待つしかない。
でも実務では、
「古い検索結果はいらないから、最新の検索だけ有効にしたい」
「画面を切り替えたら、前の画面のリクエストはもう不要」
という場面が山ほどあります。
そこで使うのが AbortController です。
まずは AbortController の最小パターンを押さえる
fetch を途中でキャンセルする一番シンプルな例
まずは「キャンセルできる fetch」を書いてみます。
const controller = new AbortController();
async function loadData() {
try {
const response = await fetch("/api/data", {
signal: controller.signal,
});
const data = await response.json();
console.log("取得成功:", data);
} catch (err) {
if (err.name === "AbortError") {
console.log("リクエストはキャンセルされました");
} else {
console.error("その他のエラー:", err);
}
}
}
// どこかのタイミングでキャンセル
controller.abort();
JavaScriptここで重要なポイントは 3 つです。
const controller = new AbortController()
→ キャンセルを管理する「コントローラ」を作る。
fetch(..., { signal: controller.signal })
→ 「この fetch は、このコントローラに紐づいています」と教える。
controller.abort()
→ 「このコントローラに紐づいている処理は、もうやめて」と指示する。
キャンセルされた側(fetch)は、AbortError というエラーを投げます。
だから catch の中で err.name === "AbortError" を見て、
「これはキャンセルだな」と判定します。
ここが重要です。
AbortController は「キャンセル用のリモコン」、signal は「そのリモコンに反応するアンテナ」、abort() は「リモコンの停止ボタン」だとイメージすると分かりやすいです。
なぜキャンセルが必要になるのか(実務のイメージ)
検索ボックスの「タイプするたびに検索」問題
よくあるのが「インクリメンタルサーチ」です。
ユーザーが文字を入力するたびに API を叩いて検索結果を出すやつ。
例えば、ユーザーがこう入力したとします。
「a」 → 検索
「ab」 → 検索
「abc」 → 検索
ネットワークが遅いと、
「a の結果」「ab の結果」「abc の結果」がバラバラのタイミングで返ってきます。
もしキャンセルをしないと、
最後に返ってきたのが「a の結果」だった場合、
画面には「abc に対する結果」ではなく「a に対する古い結果」が表示されてしまいます。
これ、ユーザーからするとかなり混乱します。
ここで AbortController を使って、
「新しい検索を投げたら、古い検索はキャンセルする」
という制御を入れると、
常に「最新の入力に対する結果」だけが画面に出るようになります。
画面遷移時に「もう不要なリクエスト」を止める
別のよくあるケースが、
「画面 A で API を叩いている途中に、ユーザーが画面 B に移動した」
という状況です。
画面 A の結果は、もう表示されることはありません。
それなのに、裏でリクエストだけは走り続けていると、
無駄にサーバー負荷が増える
返ってきた結果を誤って画面 B に反映してしまうバグの原因になる
といった問題が起きます。
AbortController を使えば、
「画面 A を離れるときに、その画面で投げたリクエストを全部キャンセルする」
という設計ができます。
ここが重要です。
キャンセル処理は「ユーザーの体験を守る」だけでなく、
「無駄な処理を減らし、バグの原因を減らす」ための実務的なテクニック です。
インクリメンタルサーチでの AbortController の実例
古い検索をキャンセルして、常に最新だけ有効にする
実際のコードに落としてみます。
let currentAbortController = null;
async function search(query) {
// すでに進行中の検索があればキャンセル
if (currentAbortController) {
currentAbortController.abort();
}
const controller = new AbortController();
currentAbortController = controller;
try {
const response = await fetch("/api/search?q=" + encodeURIComponent(query), {
signal: controller.signal,
});
const data = await response.json();
renderSearchResult(data);
} catch (err) {
if (err.name === "AbortError") {
console.log("古い検索はキャンセルされました:", query);
return;
}
console.error("検索エラー:", err);
showErrorMessage("検索に失敗しました");
} finally {
// 自分の検索が終わったあと、自分のコントローラならクリア
if (currentAbortController === controller) {
currentAbortController = null;
}
}
}
JavaScriptポイントを整理します。
新しい検索が始まるたびに new AbortController() する
始める前に、前回の currentAbortController.abort() を呼んでキャンセルする
fetch に signal: controller.signal を渡して紐づける
キャンセルされた場合は AbortError が飛んでくるので、それは「正常なキャンセル」として扱う
これで、
ユーザーが「a」「ab」「abc」と素早く入力しても、
「a」「ab」のリクエストは途中でキャンセルされ、
最終的に「abc」の結果だけが画面に反映されます。
ここが重要です。
AbortController を使うと、
「古いリクエストを“なかったこと”にして、常に最新の操作だけを有効にする」
という、とても実務的な制御が書けるようになります。
画面遷移やコンポーネント破棄時のキャンセル
「この画面が生きている間だけ有効」という考え方
SPA(シングルページアプリ)やコンポーネントベースの UI では、
「このコンポーネントが表示されている間だけリクエストを有効にしたい」
という場面がよくあります。
例えば、
ユーザー詳細画面コンポーネントがマウントされたときに API を叩き、
アンマウント(画面から消える)ときにリクエストをキャンセルする、
というイメージです。
素の JavaScript でざっくり書くと、こんな感じです。
let userAbortController = null;
async function loadUser(userId) {
userAbortController = new AbortController();
try {
const response = await fetch("/api/users/" + userId, {
signal: userAbortController.signal,
});
const data = await response.json();
renderUser(data);
} catch (err) {
if (err.name === "AbortError") {
console.log("ユーザー読み込みはキャンセルされました");
return;
}
console.error(err);
showErrorMessage("ユーザー情報の取得に失敗しました");
}
}
function destroyUserView() {
if (userAbortController) {
userAbortController.abort();
}
clearUserView();
}
JavaScriptdestroyUserView が呼ばれた時点で、
まだ loadUser の fetch が終わっていなければキャンセルされます。
これにより、
不要なリクエストを減らせる
「もう存在しない画面に対して render しようとしてエラー」みたいな事故を防げる
というメリットがあります。
AbortController を使うときのエラー処理の考え方
キャンセルは「失敗」ではなく「ユーザーの意思」
AbortController でキャンセルされたとき、
fetch は AbortError を投げます。
これは技術的には「エラー」ですが、
意味としては「ユーザーやアプリが意図的に止めた」だけです。
なので、エラー処理のときに、
「キャンセルはエラー扱いしない」
「ログも“エラー”ではなく“キャンセル”として扱う」
という分け方をすると、設計がきれいになります。
さっきの例のように、
catch (err) {
if (err.name === "AbortError") {
// 正常なキャンセルとして扱う
return;
}
// 本当のエラーだけここで扱う
}
JavaScriptというパターンを覚えておくと良いです。
ここが重要です。
AbortController を使うときは、
「キャンセルは“失敗”ではなく“やめることにした”」という意味だと理解し、
エラー処理の中で特別扱いするのがポイントです。
複数のリクエストと AbortController の設計
1 つのコントローラに複数の fetch をぶら下げる
実は、1 つの AbortController に対して、
複数の fetch を紐づけることもできます。
const controller = new AbortController();
async function loadAll() {
try {
const [userRes, postsRes] = await Promise.all([
fetch("/api/user", { signal: controller.signal }),
fetch("/api/posts", { signal: controller.signal }),
]);
const user = await userRes.json();
const posts = await postsRes.json();
renderUser(user);
renderPosts(posts);
} catch (err) {
if (err.name === "AbortError") {
console.log("全リクエストがキャンセルされました");
return;
}
console.error(err);
}
}
// どこかで
controller.abort();
JavaScriptこの場合、controller.abort() を呼ぶと、
ユーザー情報と投稿一覧の両方のリクエストがまとめてキャンセルされます。
「この画面に関するリクエストは全部まとめて止めたい」
というときに便利です。
コントローラの寿命をどう管理するか
AbortController を使うときに大事なのは、
「このコントローラはどこからどこまで有効か?」
という寿命の設計です。
例えば、
検索ボックスごとに 1 つ
画面(ページ)ごとに 1 つ
コンポーネントごとに 1 つ
といった単位で持つことが多いです。
「どの範囲のリクエストを一緒にキャンセルしたいか?」
を考えると、
コントローラをどこに置くべきかが見えてきます。
初心者として「AbortController / キャンセル処理」で本当に押さえてほしいこと
普通の async/await には「キャンセル」がない。
一度投げた非同期処理を止めたいときに使うのが AbortController。
new AbortController() でコントローラを作り、fetch(..., { signal: controller.signal }) で紐づけ、controller.abort() でキャンセルする。
キャンセルされると AbortError が投げられるので、catch の中で err.name === "AbortError" を見て、
「これは正常なキャンセル」として扱う。
実務での主な使いどころは、
「インクリメンタルサーチで古い検索を無効にする」
「画面遷移時に不要になったリクエストを止める」
「複数リクエストのうち、まとめて止めたいグループを作る」
といった場面。
そして何より、
キャンセル処理は「ユーザーの最新の操作を尊重する」ための仕組み だと捉えると、
どこに AbortController を置くべきかが見えてきます。
もし今、
「古いリクエストの結果があとから返ってきて、画面が変な状態になる」
みたいなバグに悩んでいるなら、
そこに一つ AbortController を差し込んでみてください。
コードの流れとユーザー体験が、ぐっと素直になります。
