Историски, Node.js немаше начин да откажеш операција штом ќе ја стартуваш. Пратил HTTP барање? Чекај додека не одговори или не падне по таjмаут. Читаш огромен фајл? Читај до крај. Пушти пакет промиси? Седи и гледај како ги јадат ресурсите. Механизам за „стоп, доста” едноставно не постоеше. Некои правеа свои решенија со флагови, некои користеа библиотеки како 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: таjмаут без библиотеки
Најчестиот случај е да го ограничиш времето на 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)
});
const data = await response.json();
} catch (err) {
if (err.name === 'TimeoutError') {
console.log('Таjмаут, серверот не одговори за 5 секунди');
}
}
AbortSignal.timeout() е статичка метода која создава сигнал со автоматски таjмаут. Не треба контролер, не треба 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) {
console.log('Откажано:', signal.reason);
}
// Во handler на копчето:
userController.abort('User cancelled');
// При демонтирање:
cleanupController.abort('Component unmounted');
Ќе сработи оној сигнал кој ќе пристигне прв. Останатите се игнорираат, резултантниот сигнал е веќе aborted.
File систем
fs/promises поддржува signal во readFile, writeFile, open и watch:
import { readFile } 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 {
const result = await delay(10000, 'готово', {
signal: controller.signal
});
console.log(result); // 'готово' ако стигне до крај
} catch (err) {
if (err.name === 'AbortError') {
console.log('Не дочекавме, откажавме');
}
}
Корисно за polling, retry логика, и секоја „чекај, но со можност за прекин” ситуација.
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(), асинхрон итератор по настани:
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();
req.on('close', () => controller.abort('Client disconnected'));
try {
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' });
}
});
Овој pattern заштедува серверски ресурси.
Сопствен код со поддршка за 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 });
});
}
}
// Употреба: polling со таjмаут од 30 секунди
const result = await pollUntilReady('https://api.example.com/job/123', {
signal: AbortSignal.timeout(30000),
interval: 3000,
});
signal.throwIfAborted() фрла исклучок ако сигналот веќе сработил. Повикувај го на почетокот на секој чекор за да не вршиш непотребна работа.
Откажливиот sleep е доволно чест pattern за да вреди да се изолира во утилитет:
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 при успех. Инаку тајмерот ќе сработи откако операцијата веќе ќе завршила.







