У Node.js исторически была проблема с отменой операций. Запустил HTTP‑запрос — жди, пока не ответит или не упадёт по таймауту. Читаешь огромный файл — читай до конца. Запустил пачку промисов — сиди, смотри, как они доедают ресурсы. Механизма сказать «стоп, хватит» в языке просто не было. Кто‑то мастерил свои костыли на флагах, кто‑то использовал библиотеки вроде p-cancelable, но единого стандарта не существовало.
AbortController эту проблему решает. Пришёл он из браузерного API (там его придумали для отмены fetch), но в Node.js прижился настолько, что теперь поддерживается почти везде: fetch, fs, stream, child_process, setTimeout, EventEmitter, встроенный тест‑раннер.
Разберём, как он устроен, и где вообще полезен.
Механика: контроллер и сигнал
Вся идея укладывается в два объекта.
AbortController — тот, кто отменяет. AbortSignal — тот, кто слушает отмену. Контроллер создаёт сигнал, вы передаёте сигнал в операцию, а когда нужно отменить — дёргаете abort() на контроллере.
const controller = new AbortController();
const signal = controller.signal;
// Подписываемся на отмену
signal.addEventListener('abort', () => {
console.log('Отменено!');
console.log(signal.reason); // причина отмены
});
console.log(signal.aborted); // false — пока не отменён
controller.abort('Пользователь нажал Отмена');
// → Отменено!
// → Пользователь нажал Отмена
console.log(signal.aborted); // true — уже отменён
Контроллер одноразовый. Один abort() — и всё. Повторно вызвать можно, но ничего не произойдёт — сигнал уже сработал. Если нужна новая операция с возможностью отмены — создавайте новый контроллер.
Аргумент abort() — это reason. Может быть строкой, ошибкой, чем угодно. Если не передать — по умолчанию будет DOMException с именем AbortError.
fetch: таймаут без библиотек
Самый частый кейс — ограничить время HTTP‑запроса. До AbortController это выглядело так: Promise.race([fetch(url), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000))]). Без комментариев.
Теперь:
// Способ 1: AbortSignal.timeout() — самый простой
try {
const response = await fetch('https://api.example.com/data', {
signal: AbortSignal.timeout(5000) // 5 секунд и всё
});
const data = await response.json();
} catch (err) {
if (err.name === 'TimeoutError') {
console.log('Таймаут — сервер не ответил за 5 секунд');
}
}
AbortSignal.timeout() — статический метод, который создаёт сигнал с автоматическим таймаутом. Не нужен контроллер, не нужен setTimeout, не нужен clearTimeout. Одна строка.
Но если нужна отмена по условию (а не только по времени), нужен ручной контроллер:
// Способ 2: ручной контроллер — отмена по кнопке/событию/условию
const controller = new AbortController();
// Таймаут вручную
const timerId = setTimeout(() => controller.abort('timeout'), 5000);
try {
const response = await fetch('https://api.example.com/data', {
signal: controller.signal
});
clearTimeout(timerId); // Ответ пришёл — таймер не нужен
return await response.json();
} catch (err) {
if (err.name === 'AbortError') {
console.log('Запрос отменён:', controller.signal.reason);
} else {
throw err; // Сетевая ошибка или что-то другое
}
}
AbortSignal.timeout() бросает TimeoutError, а ручной abort() без аргумента — AbortError. Если вам нужно различать «пользователь отменил» и «сервер не ответил» — используйте ручной контроллер для первого и timeout() для второго. Или передавайте разные reason в abort().
AbortSignal.any(): комбинируем условия отмены
Реальная задача: запрос должен отмениться, если прошло 5 секунд, ИЛИ если пользователь нажал «Отмена», ИЛИ если компонент размонтировался. Три причины, один fetch.
AbortSignal.any() объединяет несколько сигналов в один:
const userController = new AbortController();
const cleanupController = new AbortController();
// Отменится по ЛЮБОМУ из трёх условий
const signal = AbortSignal.any([
AbortSignal.timeout(5000), // таймаут
userController.signal, // пользователь нажал Отмена
cleanupController.signal, // компонент размонтировался
]);
try {
const response = await fetch(url, { signal });
const data = await response.json();
} catch (err) {
// signal.reason покажет причину того сигнала, который сработал первым
console.log('Отменено:', signal.reason);
}
// Где-то в обработчике кнопки:
userController.abort('User cancelled');
// Где-то при размонтировании:
cleanupController.abort('Component unmounted');
Сработает тот сигнал, который придёт первым. Остальные игнорируются — результирующий сигнал уже aborted.
Файловая система
fs/promises поддерживает signal в readFile, writeFile, open, watch:
import { readFile, writeFile } from 'node:fs/promises';
const controller = new AbortController();
// Где-то может прийти отмена
setTimeout(() => controller.abort(), 3000);
try {
const data = await readFile('/path/to/huge-file.csv', {
signal: controller.signal,
encoding: 'utf-8',
});
console.log(`Прочитано ${data.length} символов`);
} catch (err) {
if (err.name === 'AbortError') {
console.log('Чтение файла отменено — не успели за 3 секунды');
} else {
throw err;
}
}
Для writeFile — то же самое. Если отмена пришла до завершения записи, файл может быть записан частично. Учитывайте это: пишите во временный файл, потом переименовывайте.
Стримовый API (createReadStream, createWriteStream) не принимает signal напрямую. Там нужно закрывать стрим вручную или использовать pipeline() из stream/promises, который signal поддерживает:
import { pipeline } from 'node:stream/promises';
import { createReadStream, createWriteStream } from 'node:fs';
await pipeline(
createReadStream('input.csv'),
createWriteStream('output.csv'),
{ signal: AbortSignal.timeout(10000) }
);
Промис‑таймеры
Промис‑версии таймеров из node:timers/promises тоже поддерживают сигнал:
import { setTimeout as delay } from 'node:timers/promises';
const controller = new AbortController();
try {
// Ждём 10 секунд, но можем отменить раньше
const result = await delay(10000, 'готово', {
signal: controller.signal
});
console.log(result); // 'готово' — если дождались
} catch (err) {
if (err.name === 'AbortError') {
console.log('Не дождались, отменили');
}
}
Удобно для поллинга, ретраев, любых «подождать, но с возможностью прервать».
EventEmitter: автоматическая отписка
Малоизвестная, но очень полезная штучка. on() на EventEmitter принимает signal для автоматической отписки:
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
const controller = new AbortController();
emitter.on('data', (chunk) => {
console.log('Получено:', chunk);
}, { signal: controller.signal });
emitter.emit('data', 'первый'); // → Получено: первый
emitter.emit('data', 'второй'); // → Получено: второй
controller.abort();
emitter.emit('data', 'третий'); // → тишина, слушатель отписан
Ещё один кейс — events.on() (async итератор по событиям):
import { on } from 'node:events';
const controller = new AbortController();
// Асинхронный итератор по событиям — с возможностью остановки
setTimeout(() => controller.abort(), 5000);
try {
for await (const [data] of on(emitter, 'data', { signal: controller.signal })) {
console.log(data);
if (data === 'stop') controller.abort();
}
} catch (err) {
if (err.name === 'AbortError') {
console.log('Итерация остановлена');
}
}
Отмена при обрыве HTTP‑соединения
Клиент может закрыть вкладку или оборвать соединение. Если вы в этот момент ждёте ответ от тяжёлого внешнего API или считаете отчёт — зачем продолжать? Получатель ушёл.
// Express
app.get('/api/report', async (req, res) => {
const controller = new AbortController();
// Клиент закрыл соединение — отменяем всё downstream
req.on('close', () => controller.abort('Client disconnected'));
try {
// Тяжёлый запрос к внешнему API
const raw = await fetch('https://analytics.internal/heavy-report', {
signal: controller.signal,
});
const report = await raw.json();
// Тяжёлая обработка — тоже проверяем отмену
controller.signal.throwIfAborted();
const processed = processReport(report);
res.json(processed);
} catch (err) {
if (err.name === 'AbortError') {
// Клиент ушёл — ответ отправлять некому, просто выходим
return;
}
res.status(500).json({ error: 'Internal error' });
}
});
Паттерн экономит серверные ресурсы.
Свой код с поддержкой AbortSignal
Если пишете библиотеку или утилиту, добавить поддержку отмены несложно. Просто принимаете signal в options, проверяете throwIfAborted() перед каждым шагом, передаёте signal во все вложенные операции.
async function pollUntilReady(url, { signal, interval = 2000 } = {}) {
while (true) {
// Проверяем перед каждой итерацией
signal?.throwIfAborted();
try {
const res = await fetch(url, { signal });
const data = await res.json();
if (data.status === 'ready') {
return data;
}
} catch (err) {
// Если отмена — пробрасываем наверх
if (err.name === 'AbortError') throw err;
// Если сетевая ошибка — пробуем ещё раз
console.log('Retry after error:', err.message);
}
// Пауза между попытками — тоже отменяемая
await new Promise((resolve, reject) => {
const timer = globalThis.setTimeout(resolve, interval);
signal?.addEventListener('abort', () => {
clearTimeout(timer);
reject(signal.reason);
}, { once: true });
});
}
}
// Использование: поллинг с таймаутом 30 секунд
const result = await pollUntilReady('https://api.example.com/job/123', {
signal: AbortSignal.timeout(30000),
interval: 3000,
});
signal.throwIfAborted() — бросает исключение, если сигнал уже сработал. Вызывайте в начале каждого шага, чтобы не делать лишнюю работу.
Отменяемый sleep — настолько частый паттерн, что имеет смысл вынести в утилиту:
function abortableSleep(ms, { signal } = {}) {
return new Promise((resolve, reject) => {
const timer = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => {
clearTimeout(timer);
reject(signal.reason);
}, { once: true });
});
}
Некоторые ошибки
-
Переиспользование контроллера. Один контроллер — одна логическая операция. Отменили — создали новый. Если повесить один контроллер на цикл запросов и вызвать
abort()— отменятся все разом, включая будущие. -
Забыли обработать AbortError. Без
try/catchотмена уронит процесс. Всегда проверяйтеerr.name === 'AbortError'и решайте — это штатная ситуация или нет. -
AbortController на синхронном коде. JSON‑парсинг, сортировка массива, валидация — отменять нечего. AbortController для async‑операций.
-
Утечка таймеров. Если создали
setTimeoutдля ручного abort — не забудьтеclearTimeoutпри успехе. Иначе таймер сработает уже после завершения операции.

Если хочется не просто писать серверный код на JavaScript, а уверенно разбираться в архитектуре Node.js-приложений, здесь нужна не подборка приёмов, а системная практика. На курсе «Node.js разработчик» как раз дают TypeScript, базы данных, тестирование и ключевые инструменты бэкенд-разработки в связке.
Для знакомства с форматом обучения и экспертами приходите на бесплатные уроки:
-
26 марта в 20:00. «NestJS и архитектура масштабируемых серверных приложений на Node.js». Записаться
-
9 апреля в 20:00. «Работа в реальном времени на Node.js и TypeScript: создаём WebSocket-чат и разбираем архитектуру серверной части». Записаться
-
22 апреля в 20:00. «Bun + искусственный интеллект: создаём быстрый сервер нового поколения на JavaScript». Записаться
Автор: badcasedaily1


