Cancellable Promise
Promise в JavaScript отменяется не «по-настоящему» — мы отменяем не саму операцию, а интерес к её результату. Стандартный API —
AbortController/AbortSignal. Свой Cancellable Promise полезен, когда нужно отменить произвольную асинхронную работу.
Что это / Зачем
Promise в спецификации не имеет встроенной отмены. Если вы отправили HTTP-запрос — отмена Promise не остановит сетевой пакет. Отмена в JS — это:
- Игнорирование результата операции (не вызываем resolve/reject).
- По возможности — реальное прерывание операции (
xhr.abort(),fetchсAbortController, очисткаsetTimeout).
API / Синтаксис
// 1. Стандартный путь — AbortController (см. [[AbortController]])
const ctrl = new AbortController();
fetch('/api', { signal: ctrl.signal });
ctrl.abort();
// 2. Cancellable обёртка над произвольной операцией
function cancellable(promise) {
let cancelled = false;
const wrapped = new Promise((resolve, reject) => {
promise.then(
(value) => (cancelled ? reject({ cancelled: true }) : resolve(value)),
(error) => (cancelled ? reject({ cancelled: true }) : reject(error)),
);
});
wrapped.cancel() = () => { cancelled = true; };
return wrapped;
}
// 3. С функцией onCancel (полная отмена операции)
function makeCancellable(executor) {
let onCancel;
let cancelled = false;
const promise = new Promise((resolve, reject) => {
executor(
(v) => !cancelled && resolve(v),
(e) => !cancelled && reject(e),
(fn) => (onCancel = fn), // третий аргумент — регистрация cancel
);
});
promise.cancel() = () => {
cancelled = true;
if (onCancel) onCancel;
};
return promise;
}
const req = makeCancellable((resolve, reject, onCancel) => {
const xhr = new XMLHttpRequest();
xhr.onload = () => resolve(xhr.responseText);
xhr.onerror = () => reject(new Error('fail'));
xhr.open('GET', '/api');
xhr.send;
onCancel( => xhr.abort());
});
req.cancel(); // и игнор результата, и abort
Ключевые моменты
- Отменяем не операцию, а результат. Чтобы отменить операцию, нужен механизм самой операции (
AbortController,xhr.abort(),clearTimeout). - Третий параметр executor'а
onCancel— антипаттерн в API стандарта, но удобный приём для своих абстракций. - Отменённый промис остаётся в состоянии
pending, если мы не вызовем reject — это часто нежелательно (память). Лучше переводить в rejected с маркером{ cancelled: true }. - Современная альтернатива — везде использовать
AbortSignal(поддерживается fetch, addEventListener, async iterators, web streams).
Подводные камни
- Наследование от Promise (
class Cancellable extends Promise) — выглядит логично, ноthen/catchбудут возвращатьCancellable, а Promise-методы вродеPromise.allмогут вернуть Cancellable неожиданно. Часто чище — обёртка без наследования. - Cancel должен быть идемпотентен и однократен — повторный вызов не должен ломать состояние.
- Если работаете с async/await — отмена приходит как reject с особым типом ошибки (
AbortError), его надо отличать от обычной ошибки.
🎓 Источники
- 🎓 [Отмена асинхронных операций, cancellable callback and Promise в JavaScript] · 2019-05-02 · YouTube
- Тезисы:
- Отменяем НЕ операцию, а интерес к её результату.
- Реально отменимы: setTimeout (clearTimeout), XHR (abort), fetch (AbortController), подписки на события.
- Третий аргумент
onCancelв executor'е — приём для регистрации действий на отмену. - Наследование
class extends Promiseдля cancel — антипаттерн. - Синхронный cancel в момент создания корректно отменяет и не-стартовавшие подписки.
- Цитата:
«На самом деле мы отменяем не саму операцию, а результат. Операция уже идёт — её отменить может только тот, кто её запустил.»
- Тезисы:
- ⚡ [LeetCode Решаем hard задачу Design Cancellable Function] · AsForJS · 2023-11-28 · YouTube
- Тезисы:
- LeetCode: отмена generator-based async через
next/throw— отмена реализуется через бросок исключения в генератор. - Promise+race с reject-сигналом — простейший прикладной паттерн отмены.
- LeetCode: отмена generator-based async через
- Тезисы:
- ⚡ [Design Cancellable Function продолжение] · AsForJS · 2023-11-28 · YouTube