Что такое AbortController и AbortSignal в JavaScript?
AbortController и AbortSignal
AbortController — это встроенный браузерный (и Node.js) API, который позволяет отменять асинхронные операции. Он был введён вместе с Fetch API и решает проблему отсутствия нативного механизма отмены промисов.
Как это работает
При создании AbortController он автоматически создаёт связанный объект AbortSignal, доступный через свойство .signal. Этот сигнал передаётся в операцию, которую нужно сделать отменяемой. Когда вызывается метод .abort() на контроллере, сигнал переходит в состояние aborted: true и генерирует событие abort.
const controller = new AbortController();
const signal = controller.signal;
console.log(signal.aborted); // false
controller.abort();
console.log(signal.aborted); // true
Использование с fetch
Наиболее распространённый сценарий — отмена HTTP-запросов:
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('Запрос был отменён');
} else {
console.error('Сетевая ошибка:', err);
}
});
// Отменяем запрос через 3 секунды
setTimeout(() => controller.abort(), 3000);
Важно различать AbortError и другие ошибки, чтобы не показывать пользователю ложные сообщения об ошибках.
Таймаут через AbortSignal.timeout()
С ES2022 появился удобный статический метод для создания сигнала с таймаутом:
// Автоматически отменит запрос через 5 секунд
fetch('/api/data', { signal: AbortSignal.timeout(5000) });
Использование в пользовательских асинхронных операциях
AbortController можно использовать не только с fetch — любая асинхронная операция может поддерживать отмену:
function delay(ms, signal) {
return new Promise((resolve, reject) => {
const timer = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => {
clearTimeout(timer);
reject(new DOMException('Операция отменена', 'AbortError'));
});
});
}
const controller = new AbortController();
delay(5000, controller.signal)
.then(() => console.log('Готово'))
.catch(err => console.log(err.message));
controller.abort(); // Отменяем немедленно
Объединение нескольких сигналов
В современных средах можно объединить несколько сигналов с помощью AbortSignal.any():
const userController = new AbortController();
const timeoutSignal = AbortSignal.timeout(10000);
// Отменится при первом сработавшем сигнале
const combinedSignal = AbortSignal.any([
userController.signal,
timeoutSignal
]);
fetch('/api/data', { signal: combinedSignal });
Практический паттерн в React
Очень часто AbortController используется в useEffect для отмены запросов при размонтировании компонента:
useEffect(() => {
const controller = new AbortController();
fetch('/api/user', { signal: controller.signal })
.then(r => r.json())
.then(setUser)
.catch(err => {
if (err.name !== 'AbortError') setError(err);
});
return () => controller.abort(); // Очистка при размонтировании
}, [userId]);
Что хочет услышать интервьюер
Понимание назначения AbortController — отмена асинхронных операций, прежде всего fetch-запросов
Знание связки AbortController ↔ AbortSignal: контроллер управляет, сигнал передаётся в операцию
Умение различать AbortError от других ошибок в catch-блоке
Знакомство с практическими сценариями: отмена при размонтировании React-компонента, debounce запросов, таймауты
Понимание, что один контроллер можно использовать для отмены нескольких запросов одновременно
Пример: Базовое использование с fetch и обработка отмены
async function fetchUserData(userId: string, signal: AbortSignal) {
const response = await fetch(`/api/users/${userId}`, { signal });
if (!response.ok) {
throw new Error(`HTTP ошибка: ${response.status}`);
}
return response.json();
}
// Использование
const controller = new AbortController();
fetchUserData('42', controller.signal)
.then(user => console.log('Пользователь:', user))
.catch((err: Error) => {
if (err.name === 'AbortError') {
console.log('Запрос отменён пользователем');
} else {
console.error('Ошибка загрузки:', err.message);
}
});
// Отменяем при уходе пользователя со страницы
window.addEventListener('beforeunload', () => controller.abort());
Пример: Паттерн в React: отмена при смене зависимости
import { useEffect, useState } from 'react';
interface User {
id: string;
name: string;
}
function useUser(userId: string) {
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
// Создаём контроллер внутри эффекта
const controller = new AbortController();
setLoading(true);
setError(null);
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error('Ошибка сервера');
return res.json() as Promise<User>;
})
.then(data => {
setUser(data);
setLoading(false);
})
.catch((err: Error) => {
// Игнорируем AbortError — это штатная отмена
if (err.name !== 'AbortError') {
setError(err.message);
setLoading(false);
}
});
// Отменяем запрос при смене userId или размонтировании
return () => controller.abort();
}, [userId]);
return { user, error, loading };
}
Пример: Отменяемая пользовательская асинхронная операция
// Реализация задержки с поддержкой отмены
function cancellableDelay(ms: number, signal?: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
// Если сигнал уже отменён — немедленно отклоняем
if (signal?.aborted) {
return reject(new DOMException('Уже отменено', 'AbortError'));
}
const timer = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => {
clearTimeout(timer);
reject(new DOMException('Задержка отменена', 'AbortError'));
}, { once: true }); // once: true — автоматически снимает обработчик
});
}
// Использование с AbortSignal.timeout()
async function pollStatus(signal: AbortSignal) {
while (!signal.aborted) {
const status = await fetch('/api/status', { signal }).then(r => r.json());
if (status.done) break;
// Ждём 2 секунды перед следующим опросом
await cancellableDelay(2000, signal);
}
}
// Опрос с автоматическим таймаутом 30 секунд
pollStatus(AbortSignal.timeout(30_000))
.catch(err => {
if (err.name === 'TimeoutError') console.log('Истекло время ожидания');
else if (err.name === 'AbortError') console.log('Опрос отменён');
});
Типичные ошибки
Не обрабатывают AbortError отдельно, показывая пользователю ошибку там, где запрос был намеренно отменён
Создают новый AbortController на каждый рендер в React вместо того, чтобы создавать его внутри useEffect
Забывают вызвать controller.abort() в функции очистки useEffect, что приводит к утечкам и race condition
Путают AbortController с механизмом отмены промисов — промисы сами по себе нельзя отменить, AbortController лишь сигнализирует об отмене
Не знают о современных статических методах AbortSignal.timeout() и AbortSignal.any(), предлагая более громоздкие решения


