AbortControllerの使い方!fetchのキャンセル方法やAbortSignalとの違いを解説

JavaScriptでfetch()を使ってAPI通信をしていると、「実行中のリクエストを途中でキャンセルしたい」という場面があります。

例えば、検索画面でユーザーが別のキーワードを入力したため古いリクエストが不要になった場合や、画面を移動したため実行中のリクエストが不要になった場合です。

このような処理を実現するために使用できるのが、AbortControllerとAbortSignalです。

名前が似ているため、

  • AbortControllerとは何?
  • AbortSignalとは何?
  • AbortControllerとAbortSignalは何が違うの?
  • fetch()をどうやってキャンセルするの?

と疑問に思うこともあるでしょう。

この記事では、AbortControllerとAbortSignalの基本的な意味や違いから、fetch()を途中で中断する方法まで、シンプルなサンプルコードを用いてわかりやすく解説します。

AbortControllerとは?

AbortControllerとは?

AbortControllerとは、fetch()などの処理に中断を通知するためのコントローラーを作成するWeb APIです。

例えば、次のようにAbortControllerのインスタンスを作成できます。

const controller = new AbortController();

作成したcontrollerでは、主に次の2つを使用します。

プロパティ・メソッド説明
controller.signal中断を通知するためのAbortSignalを取得する
controller.abort()中断を通知する

つまり、簡単にいうと、AbortControllerは処理を中断するためのコントローラー(操作側)です。

AbortSignalとは?

AbortSignalとは?

AbortSignalとは、処理に「中断された」という状態を伝えるためのオブジェクトです。

AbortControllerを作成すると、そのコントローラーに対応するAbortSignalをsignalプロパティから取得できます。

例えば、次のように記述します。

const controller = new AbortController();
const signal = controller.signal;

取得したsignalは、fetch()のオプションとして指定できます。

fetch()は、指定したURLへHTTPリクエストを送信するための関数です。基本的には、第1引数にリクエスト先のURL、第2引数にリクエストの設定をまとめたオブジェクトを指定します。

fetch("https://example.com/api/users", {
  // リクエストのオプション
});

第2引数のオブジェクトには、HTTPメソッドを指定するmethodやリクエストヘッダーを指定するheadersなど、さまざまなオプションを指定できます。

AbortSignalを使用する場合は、このオプションのsignalプロパティにAbortSignalのインスタンスを指定します。

fetch("https://example.com/api/users", {
  signal: signal
});

この例では、先ほど取得したsignalをfetch()へ渡しています。

なお、プロパティ名と変数名がどちらもsignalなので、JavaScriptの省略記法を使用して次のように書くこともできます。

fetch("https://example.com/api/users", {
  signal
});

また、変数へ取り出さず、次のようにcontroller.signalを直接指定することもできます。

fetch("https://example.com/api/users", {
  signal: controller.signal
});

その後、controller.abort()を実行するとsignalが中断状態になり、そのsignalを使用しているfetch()に中断が伝わります。

つまり、簡単にいうと、AbortSignalは中断することを対象の処理へ伝える役割を持っています。

AbortControllerとAbortSignalの違い

AbortControllerとAbortSignalは一緒に使用することが多いため、最初は違いが分かりにくいかもしれません。

それぞれの役割を簡単に整理すると、次のようになります。

AbortControllerAbortSignal
主な役割中断を指示する中断を処理へ伝える
主な使い方abort()を実行するfetch()などへ渡す
イメージ中断する側中断を伝える側

処理の流れは、次のように考えると分かりやすいでしょう。

AbortController
      │
      ├─ controller.signal ──→ AbortSignal ──→ fetch() に渡す
      │
      └─ controller.abort()
               │
               ↓
        AbortSignal が中断状態になる
               │
               ↓
          fetch() が中断される

AbortControllerが直接fetch()を操作しているわけではありません。

AbortControllerから取得したAbortSignalをfetch()へ渡しておき、abort()が呼び出されたときに、そのAbortSignalを通して中断が通知される仕組みです。

AbortControllerでfetchを中断する方法

それでは、実際にAbortControllerを使用してfetch()を中断してみましょう。

基本的な流れは次の3ステップです。

  • AbortControllerを作成する
  • signalをfetch()へ渡す
  • abort()を実行する

例えば、次のように記述できます。

const controller = new AbortController();

fetch("https://example.com/api/users", {
  signal: controller.signal
});

controller.abort();

まず、次の部分でAbortControllerを作成しています。

const controller = new AbortController();

次に、controller.signalをfetch()のsignalオプションへ渡しています。

fetch("https://example.com/api/users", {
  signal: controller.signal
});

これによって、このfetch()はcontrollerからの中断通知を受け取れるようになります。

最後に、次のようにabort()を実行します。

controller.abort();

abort()が実行されるとcontroller.signalが中断状態になり、実行中のfetch()が中断されます。

fetchが中断されるとAbortErrorが発生する

引数なしでcontroller.abort()を実行してfetch()を中断すると、通常はfetch()に関連するPromiseが拒否され、AbortErrorという名前のDOMExceptionを受け取ります。

そのため、実際にはtry...catchを使用してエラーを処理します。

例えば、次のコードではfetch()を実行し、1秒後に中断しています。

const controller = new AbortController();

async function getUsers() {
  try {
    const response = await fetch("https://example.com/api/users", {
      signal: controller.signal
    });

    const data = await response.json();
    console.log(data);
  } catch (error) {
    if (error.name === "AbortError") {
      console.log("リクエストが中断されました");
    } else {
      console.error(error);
    }
  }
}

getUsers();

setTimeout(() => {
  controller.abort();
}, 1000);

fetch()が完了する前にcontroller.abort()が実行された場合、fetch()が中断されます。

そして、catchの中でAbortErrorかどうかを確認しています。

if (error.name === "AbortError") {
  console.log("リクエストが中断されました");
}

これによって、中断によるエラーとネットワークエラーなどの別のエラーを分けて処理できます。

ボタンを押したときにfetchを中断する

実際のWebアプリでは、ユーザーがキャンセルボタンを押したときに通信を中断するといった使い方ができます。

例えば、HTMLに次のボタンがあるとします。

<button id="cancel">キャンセル</button>

JavaScriptでは、次のように記述できます。

const controller = new AbortController();

async function getUsers() {
  try {
    const response = await fetch("https://example.com/api/users", {
      signal: controller.signal
    });

    const data = await response.json();
    console.log(data);
  } catch (error) {
    if (error.name === "AbortError") {
      console.log("キャンセルされました");
    } else {
      console.error(error);
    }
  }
}

getUsers();

document.getElementById("cancel").addEventListener("click", () => {
  controller.abort();
});

ユーザーが「キャンセル」ボタンをクリックするとcontroller.abort()が実行され、実行中のfetch()が中断されます。

このように、AbortControllerを使用するとユーザー操作に応じて通信をキャンセルできます。

AbortSignalのabortedで中断されたか確認する

AbortSignalにはabortedプロパティがあり、中断されたかどうかを確認できます。

例えば、次のコードを見てみましょう。

const controller = new AbortController();

console.log(controller.signal.aborted);
// false

controller.abort();

console.log(controller.signal.aborted);
// true

abort()を実行する前はfalseですが、実行した後はtrueになります。

そのため、次のように中断状態を確認することもできます。

if (controller.signal.aborted) {
  console.log("中断されています");
}

abortedは中断状態を確認するためのプロパティであり、中断を実行するものではありません。

AbortSignalのabortイベントを使用する

AbortSignalでは、中断されたときに発生するabortイベントを監視することもできます。

例えば、次のコードを実行します。

const controller = new AbortController();

controller.signal.addEventListener("abort", () => {
  console.log("中断されました");
});

controller.abort();

// 実行結果
// 中断されました

controller.abort()が実行されるとsignalのabortイベントが発生するため、登録した処理が実行されます。

fetch()ではsignalを渡すだけで中断処理に対応できますが、自分で作成した非同期処理をAbortSignalに対応させたい場合などには、このabortイベントを利用できます。

AbortControllerは複数の処理をまとめて中断できる

同じAbortSignalを複数の処理へ渡すこともできます。

例えば、2つのfetch()に同じcontroller.signalを渡してみましょう。

const controller = new AbortController();

fetch("https://example.com/api/users", {
  signal: controller.signal
});

fetch("https://example.com/api/posts", {
  signal: controller.signal
});

controller.abort();

どちらのfetch()にも同じAbortSignalが渡されています。

そのため、controller.abort()を実行すると、同じsignalを使用している両方のリクエストに中断が通知されます。

複数の関連する処理をまとめてキャンセルしたい場合に便利です。

一度中断したAbortSignalは再利用できない

AbortSignalを使用するときに注意したいのが、一度中断されたAbortSignalを元の状態には戻せないことです。

例えば、次のコードではcontroller.abort()を実行した時点でcontroller.signalが中断状態になります。

const controller = new AbortController();

controller.abort();

console.log(controller.signal.aborted);
// true

このsignalを新しいfetch()へ渡すと、すでに中断されているため、fetch()が返すPromiseはすぐに拒否されます。

新しい処理を開始したい場合は、新しいAbortControllerを作成します。

const controller1 = new AbortController();

controller1.abort();

const controller2 = new AbortController();

fetch("https://example.com/api/users", {
  signal: controller2.signal
});

「1回の処理につき必要に応じて新しいAbortControllerを作る」と考えると分かりやすいでしょう。

AbortSignal.timeout()で一定時間後に中断する

一定時間が経過したら自動的に処理を中断したい場合は、AbortSignal.timeout()を使用する方法もあります。

例えば、次のコードでは5秒経過するとfetch()が中断されます。

try {
  const response = await fetch("https://example.com/api/users", {
    signal: AbortSignal.timeout(5000)
  });

  const data = await response.json();
  console.log(data);
} catch (error) {
  if (error.name === "TimeoutError") {
    console.log("タイムアウトしました");
  } else {
    console.error(error);
  }
}

AbortSignal.timeout(5000)は、5,000ミリ秒(5秒)後に自動的に中断されるAbortSignalを作成します。

AbortControllerを自分で作成してsetTimeout()からabort()を呼び出す必要がないため、タイムアウトだけが目的であれば簡潔に記述できます。

なお、AbortSignal.timeout()は比較的新しい機能のため、古いブラウザーも対象にする場合は対応状況を確認して使用しましょう。

AbortSignal.any()で複数の中断条件を組み合わせる

「キャンセルボタンが押されたとき」または「一定時間が経過したとき」のどちらでも中断したい場合があります。

このような場合はAbortSignal.any()を使用して、複数のAbortSignalを1つにまとめることができます。

例えば、次のように記述します。

const controller = new AbortController();

const signal = AbortSignal.any([
  controller.signal,
  AbortSignal.timeout(5000)
]);

try {
  const response = await fetch("https://example.com/api/users", {
    signal
  });

  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}

この例では、次のどちらかが発生するとfetch()が中断されます。

  • controller.abort()が実行される
  • 5秒経過する

AbortSignal.any()は、複数の中断条件を組み合わせたい場合に便利です。

ただし、AbortSignal.any()も比較的新しい機能のため、古いブラウザーを対象にする場合は対応状況を確認しましょう。

AbortControllerとAbortSignalを使用する主な場面

AbortControllerとAbortSignalは、主に不要になった非同期処理を途中で終了させたい場合に使用できます。

例えば、次のような場面があります。

  • APIリクエストをキャンセルする
  • 一定時間経過した通信をタイムアウトさせる
  • 検索キーワードが変更されたときに古い検索リクエストを中断する
  • 画面遷移などによって不要になったリクエストを中断する
  • 複数の関連する処理をまとめて中断する

特にfetch()を使用したAPI通信では、不要になったリクエストを中断するためによく利用されます。

本記事のまとめ

この記事では「AbortControllerとAbortSignal」について、以下の内容を説明しました。

  • AbortControllerは処理に中断を通知するために使用する
  • AbortSignalは中断状態を対象の処理へ伝える
  • controller.signalをfetch()へ渡し、controller.abort()を実行するとリクエストを中断できる
  • 中断されたAbortSignalは元の状態に戻せないため、新しい処理では必要に応じて新しいAbortControllerを作成する
  • AbortSignal.timeout()を使用すると一定時間後に自動で中断できる
  • AbortSignal.any()を使用すると複数の中断条件を組み合わせられる

AbortControllerとAbortSignalは名前が似ていますが、「AbortControllerが中断を指示し、AbortSignalがその中断を処理へ伝える」と覚えておくと、それぞれの役割を理解しやすくなります。

お読みいただきありがとうございました。

スポンサーリンク