Отменяемые промисы

Промисы не имеют встроенного механизма отмены. Из-за этого разработчики придумывают различные хаки, которые порождают некоторые проблемы.

Дополнительный метод .cancel()

Помимо того, что метод .cancel() делает промис нестандартным, нарушается еще один из главных принципов — только функция, которая создала промис, может изменять его состояние.

Отсутствие очистки после отмены

Некоторые используют Promise.race() в качестве механизма отмены. Однако, в этом случае у функции, создающей промис, нет возможности узнать, что произошла отмена и нужно освободить некоторые ресурсы.

speculation

Эрик Элиот написал маленький модуль speculation, который решает перечисленные выше проблемы.

Сигнатура функций:

speculation(fn: PromiseFunction, shouldCancel: Promise) => Promise

По аналогии с конструктором Promise, первым аргументом передается функция «исполнитель», а вторым аргументом передается промис для отмены.

PromiseFunction(resolve: Function, reject: Function, onCancel: Function) => Void

Функция-исполнитель принимает 3 аргумента и реализует логику разрешения, отклонения и отмены. Вот почему у самого промиса не нужен дополнительный метод .cancel(), и при этом вся логика, связанная с отменой, реализована внутри функции, которая создаёт этот промис.

import speculation from 'speculation';

function doWork(cancel = Promise.reject()) {
  speculation(
    (resolve, reject, onCancel) => {
      // делаем вычисления, которые завершаем вызовом resolve()

      onCancel(() => {
        // отменяем вычисления и освобождаем ресурсы
        reject(new Error('Cancelled'));
      });
    },
    cancel
  );
}

Отменённый промис получает статус rejected.

AbortController и AbortSignal

В браузерах начал появляться немного другой механизм для отмены асинхронных операций, возвращающих промисы. Например, fetch() может принимать объект типа AbortSignal и останавливать запрос.

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

const request = fetch(url, {signal});

// запрос можно отменить, если необходимо
controller.abort();

Значение signal.aborted меняется только из контроллера. Одновременно с этим испускается событие abort. В спецификации дан пример, как можно применять AbortController и AbortSignal для своих API.

Отменённый промис так же получает статус rejected, а значение устанавливается в new DOMException('Aborted', 'AbortError').