AbortControllerを使って、Promiseを中断する方法

はいさい！ちゅらデータぬオースティンやいびーん

# 概要
<a href="https://developer.mozilla.org/ja/docs/Web/API/AbortController">AbortController API</a>を使って、Promiseを中断・強制的に拒否する方法を紹介します！

## 背景
非同期処理を、Promiseの外から止めたい！と、思ったことはありませんでしょうか？

社内で、Fetch API以外の非同期処理の作業を中断する方法がないか議論がありましたが、筆者はAbort Controllerを使ったらいいのではないかという考えで一つの解決法を思いつきました。

Abort ControllerはFetch APIのリクエストを中断するのに使われることは周知の通りですが、Abort Controllerは実に多くの使い方があります。

以前の記事で紹介したように、Abort ControllerでEventListenerを外すこともできます。

https://qiita.com/tronicboy/items/31c1f60daf26edc9cefb

# コード

## Abort ControllerのsignalにEventListenerを追加する
Abort ControllerのAbortController.signalは、AbortController.abort()が実行された時に、"abort"というイベントを配信します。
AbortController.signalにこの"abort"イベントを受信するEventListenerを追加することが可能です。

https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/abort_event

```javascript
const controller = new AbortController();
const { signal } = controller;

signal.addEventListener("abort", () => {
  console.log("Aborted!");
});

controller.abort(); // "Aborted!"
```

## PromiseでAbort Controllerを使って拒否する
次は、上記の手法を活かして、Promiseの外からPromiseを拒否させます。

```javascript
const controller = new AbortController();
const { signal } = controller;

const promise = new Promise((resolve, reject) => {
  let count = 10;

  const intervalId = setInterval(() => {
    count--;
    console.log("Count Down set in Executor", count);
    if (count < 1) {
      clearInterval(intervalId);
      resolve(48);
    }
  }, 1000);

  signal.addEventListener(
    "abort",
    () => {
      clearInterval(intervalId);
      reject(20);
    },
    {
      once: true,
    }
  );
});

promise.then((num) => console.log("Resolved! ", num)).catch((num) => console.log("Rejected! ", num));

setTimeout(() => controller.abort(), 3000);
```
本来なら、10秒以上経って、解決されるはずのPromiseが、3秒くらい経って拒否されるのです！

Promiseを拒否させて、履行状態にしたのですが、ここで注意しないといけないのは、Promiseが履行されたからといって、windowに対して付けたsetIntervalは続きます。

signal.addEventListenerでは、onceの設定を正にすることで、signalに付けたEventListenerが呼び出された時に、自動的に取り外されます。

# 現実的な例：File Reader API
次の例では、File Reader APIでファイルを開こうとしたが、時間がかかりすぎたので、中断したいという前提のコードです。

File Reader APIのPromise化の仕方については、以前書いた記事をご参考ください。

https://qiita.com/tronicboy/items/69e4ddb03c10f53ff18c

```javascript
const controller = new AbortController();
const { signal } = controller;

const fileReadPromise = new Promise((resolve, reject) => {
  const file = document.querySelector("input[type=file]").files[0];
  const reader = new FileReader();

  reader.onload = () => {
    resolve(reader.result);
  };

  reader.onerror = () => {
    reject(reader.error);
  };

  signal.addEventListener(
    "abort",
    () => {
      reader.abort();
      reject(new DOMException("File Read Aborted."));
    },
    { once: true }
  );

  reader.readAsDataURL(file);
});

fileReadPromise.then((dataURL) => {
  const img = document.querySelector("img#preview");
  img.setAttribute("src", dataURL);
});

controller.abort(); // File Read Aborted.
```
ここで、cancelCallbackで強制的にFile Readerの読み込み作業を中断させた上で、Promiseを拒否しているので、実際のファイルの読み込み作業はPromiseが履行状態になった時に、止まっているはずです。

signal.addEventListenerでは、onceの設定を正にすることで、signalに付けたEventListenerが呼び出された時に、自動的に取り外されます。

# まとめ
いかがでしょうか？Abort Controllerはとても便利な技術で、何かを止めたいという時にとっておきの手段です。

ぜひ他の応用例を筆者に共有していただければと思います！