Пошук уроків, статей та іншого контенту
Скасовуватимемо fetch та інші підтримувані операції через AbortController і правильно оброблятимемо скасування.
AbortControllerAbortController — це стандартний механізм скасування асинхронних операцій у JavaScript.
Він складається з двох об’єктів:
AbortController — керує скасуванням;
AbortSignal — передається в операцію та повідомляє їй про скасування.
Основний сценарій:
const controller = new AbortController();
someAsyncOperation({
signal: controller.signal
});
// В іншому місці програми
controller.abort();Після виклику abort():
controller.signal.aborted стає true;
операція, яка підтримує цей сигнал, отримує повідомлення про скасування;
зазвичай асинхронна операція завершується помилкою.
Один контролер можна використовувати для кількох операцій, якщо їх потрібно скасувати разом.
fetchУ Node.js сучасних версій fetch підтримує параметр signal.
const controller = new AbortController();
const response = await fetch("https://example.com", {
signal: controller.signal
});Щоб скасувати запит, потрібно викликати:
controller.abort();Після цього fetch відхилить проміс. Важливо відрізняти скасування від інших помилок мережі або сервера.
const http = require("node:http");
const server = http.createServer((request, response) => {
// Імітуємо повільний HTTP-сервер
setTimeout(() => {
response.writeHead(200, {
"Content-Type": "application/json"
});
response.end(JSON.stringify({ message: "Готово" }));
}, 3000);
});
server.listen(0, async () => {
const { port } = server.address();
const controller = new AbortController();
const cancelTimer = setTimeout(() => {
controller.abort();
}, 500);
try {
const response = await fetch(`http://localhost:${port}`, {
signal: controller.signal
});
const data = await response.json();
console.log(data);
} catch (error) {
if (controller.signal.aborted) {
console.log("Запит скасовано");
} else {
console.error("Сталася помилка запиту:", error);
}
} finally {
clearTimeout(cancelTimer);
server.close();
}
});У цьому прикладі:
локальний сервер відповідає лише через три секунди;
таймер скасовує запит через 500 мілісекунд;
fetch завершується помилкою;
перевірка controller.signal.aborted показує, що помилка спричинена саме скасуванням.
fetch не зупиняє виконання всього Node.js-процесу. Він скасовує лише конкретну операцію, якій передали відповідний signal.
Зазвичай функція, яка виконує асинхронну операцію, приймає signal у параметрах. Це дає коду, який викликає функцію, можливість керувати її життєвим циклом.
async function loadUser(userId, { signal } = {}) {
const response = await fetch(
`https://api.example.com/users/${userId}`,
{ signal }
);
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
return response.json();
}
async function main() {
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, 1000);
try {
const user = await loadUser(42, {
signal: controller.signal
});
console.log(user);
} catch (error) {
if (controller.signal.aborted) {
console.log("Завантаження користувача скасовано");
} else {
throw error;
}
}
}
main().catch((error) => {
console.error("Непередбачена помилка:", error);
});Функція loadUser не створює власний контролер. Вона лише використовує сигнал, отриманий від викликача. Це важливо, оскільки викликаюча сторона повинна контролювати скасування операції.
Метод abort() може приймати причину:
const controller = new AbortController();
controller.abort(new Error("Користувач залишив сторінку"));Причину можна отримати через signal.reason:
const controller = new AbortController();
controller.abort("Операція більше не потрібна");
console.log(controller.signal.aborted); // true
console.log(controller.signal.reason); // "Операція більше не потрібна"Причина корисна, коли потрібно розрізнити різні сценарії скасування. Проте для обробки скасування надійніше спочатку перевіряти signal.aborted, а не покладатися лише на назву або тип помилки.
Для обмеження часу очікування можна використати AbortSignal.timeout():
async function loadData() {
const signal = AbortSignal.timeout(2000);
try {
const response = await fetch("https://example.com", {
signal
});
return await response.text();
} catch (error) {
if (signal.aborted) {
console.log("Запит перевищив допустимий час");
return null;
}
throw error;
}
}
loadData().catch((error) => {
console.error("Помилка:", error);
});AbortSignal.timeout(2000) створює сигнал, який автоматично переходить у стан скасування через 2000 мілісекунд.
Такий підхід зручний, коли не потрібно викликати abort() вручну.
AbortSignal підтримують не лише HTTP-запити. Наприклад, таймери з модуля node:timers/promises також можна скасувати.
const { setTimeout: delay } = require("node:timers/promises");
async function run() {
const controller = new AbortController();
const timer = setTimeout(() => {
controller.abort();
}, 500);
try {
await delay(3000, undefined, {
signal: controller.signal
});
console.log("Очікування завершено");
} catch (error) {
if (controller.signal.aborted) {
console.log("Очікування скасовано");
} else {
throw error;
}
} finally {
clearTimeout(timer);
}
}
run().catch(console.error);Щоб операція справді реагувала на скасування, потрібно передати їй signal. Сам факт створення AbortController не зупиняє довільний проміс.
Сигнал може бути скасований ще до запуску операції:
async function processData({ signal } = {}) {
if (signal?.aborted) {
throw signal.reason ?? new Error("Операцію скасовано");
}
// Виконання тривалої операції
return "Результат";
}
async function main() {
const controller = new AbortController();
controller.abort();
try {
await processData({
signal: controller.signal
});
} catch (error) {
if (controller.signal.aborted) {
console.log("Операцію не було запущено");
} else {
throw error;
}
}
}
main();Така перевірка корисна для власних функцій, особливо якщо між отриманням сигналу та запуском операції є додатковий код.
Один сигнал можна передати кільком асинхронним операціям:
const controller = new AbortController();
const firstRequest = fetch("https://example.com/first", {
signal: controller.signal
});
const secondRequest = fetch("https://example.com/second", {
signal: controller.signal
});
setTimeout(() => {
controller.abort();
}, 1000);Виклик controller.abort() повідомить про скасування обом запитам.
Це зручно, коли всі операції належать до одного сценарію. Наприклад, завантаження даних для одного екрана можна скасувати, якщо користувач перейшов до іншого екрана.
Скасування — це очікуваний сценарій, а не обов’язково помилка програми. Тому його краще обробляти окремо:
try {
const response = await fetch(url, {
signal
});
return await response.json();
} catch (error) {
if (signal.aborted) {
return null;
}
throw error;
}Не варто безумовно ігнорувати всі помилки:
try {
await fetch(url, { signal });
} catch {
// Погано: тут приховуються також проблеми мережі та програмні помилки
}Якщо операція не була скасована, помилку потрібно передати далі або обробити як справжню помилку.
async function loadData(signal) {
const controller = new AbortController();
return fetch(url, {
signal: controller.signal
});
}У цьому випадку зовнішній сигнал не використовується, а викликаюча сторона не може скасувати запит.
Краще передавати отриманий сигнал безпосередньо:
async function loadData(signal) {
return fetch(url, { signal });
}Неправильно:
fetch(url, {
signal: controller
});Правильно:
fetch(url, {
signal: controller.signal
});Операціям потрібен саме AbortSignal.
Після виклику abort() контролер залишається скасованим. Його не можна повернути до активного стану.
const controller = new AbortController();
controller.abort();
// Цей запит одразу буде скасовано
fetch(url, {
signal: controller.signal
});Для нової незалежної операції потрібно створити новий контролер:
const newController = new AbortController();
fetch(url, {
signal: newController.signal
});AbortController не може автоматично зупинити довільну асинхронну функцію:
function customOperation() {
return new Promise((resolve) => {
setTimeout(resolve, 3000);
});
}Якщо ця функція не перевіряє сигнал і не передає його в підтримувану операцію, виклик abort() нічого не змінить.
Для власних операцій потрібно явно додати підтримку сигналу:
function customOperation({ signal } = {}) {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(signal.reason ?? new Error("Операцію скасовано"));
return;
}
const timer = setTimeout(() => {
cleanup();
resolve("Готово");
}, 3000);
const onAbort = () => {
clearTimeout(timer);
cleanup();
reject(signal.reason ?? new Error("Операцію скасовано"));
};
const cleanup = () => {
signal?.removeEventListener("abort", onAbort);
};
signal?.addEventListener("abort", onAbort, { once: true });
});
}Тут функція:
перевіряє, чи сигнал уже скасований;
реагує на подію abort;
очищає таймер;
видаляє обробник події після завершення.
AbortController використовується для керування скасуванням асинхронних операцій.
AbortSignal передається в fetch та інші API, які підтримують скасування.
Операція скасовується викликом controller.abort().
Стан скасування перевіряється через signal.aborted.
Для тайм-аутів зручно використовувати AbortSignal.timeout().
Скасування потрібно обробляти окремо від інших помилок.
Один сигнал може керувати кількома операціями.
Після скасування AbortController не можна повторно активувати.
Довільні проміси не скасовуються автоматично: функція повинна явно підтримувати AbortSignal.