AbortController
AbortController — интерфейс для отмены асинхронных операций (fetch-запросов, event listeners, потоков).
Зачем нужно
Пользователь переключил страницу, но запрос ещё летит. Без отмены: ответ придёт и обновит уже ненужный UI, потратит трафик и может вызвать ошибки. AbortController позволяет отменить операцию чисто.
Где используется
- Отмена fetch-запросов при навигации
- Таймаут для сетевых операций
- Очистка event listeners
- Отмена при перезапросе (поисковые подсказки)
- React: отмена в useEffect cleanup
Предпосылки
Основной API
// 1. Создать контроллер
const controller = new AbortController();
// 2. Получить signal
const signal = controller.signal;
// 3. Передать signal в операцию
fetch('/api/data', { signal })
.then(r => r.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('Запрос отменён');
} else {
console.error('Ошибка:', err);
}
});
// 4. Отменить
controller.abort();
// Можно передать причину
controller.abort('Пользователь отменил');
controller.abort(new Error('Timeout'));
Свойства signal
const controller = new AbortController();
const { signal } = controller;
// Проверка состояния
console.log(signal.aborted); // false
// Слушатель на отмену
signal.addEventListener('abort', () => {
console.log('Операция отменена');
console.log('Причина:', signal.reason);
});
controller.abort('user cancelled');
console.log(signal.aborted); // true
console.log(signal.reason); // 'user cancelled'
Таймаут для fetch
// Способ 1: Ручной таймаут
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout( => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error(`Запрос к ${url} превысил ${timeoutMs}ms`);
}
throw error;
} finally {
clearTimeout(timeoutId); // Очистить таймер если запрос успел
}
}
// Способ 2: AbortSignal.timeout (современный)
const response = await fetch('/api/data', {
signal: AbortSignal.timeout(5000) // Встроенный таймаут
});
// Способ 3: Комбинация — таймаут + ручная отмена
const controller = new AbortController();
const signal = AbortSignal.any([
controller.signal, // Ручная отмена
AbortSignal.timeout(5000) // Таймаут
]);
fetch('/api/data', { signal });
// Можно отменить вручную ИЛИ по таймауту
controller.abort();
Отмена при перезапросе
// Типичный сценарий: поисковые подсказки
let currentController = null;
async function search(query) {
// Отменить предыдущий запрос
if (currentController) {
currentController.abort();
}
currentController = new AbortController();
try {
const response = await fetch(`/api/search?q=${query}`, {
signal: currentController.signal
});
const results = await response.json();
renderResults(results);
} catch (error) {
if (error.name !== 'AbortError') {
console.error(error);
}
// AbortError — это нормально, просто игнорируем
}
}
// Вызывается при каждом нажатии клавиши
input.addEventListener('input', (e) => {
search(e.target.value);
});
Очистка event listeners
// AbortController как замена removeEventListener
const controller = new AbortController();
window.addEventListener('resize', handleResize, { signal: controller.signal });
window.addEventListener('scroll', handleScroll, { signal: controller.signal });
document.addEventListener('click', handleClick, { signal: controller.signal });
// Удалить ВСЕ слушатели одной командой
controller.abort();
// Вместо:
// window.removeEventListener('resize', handleResize);
// window.removeEventListener('scroll', handleScroll);
// document.removeEventListener('click', handleClick);
React useEffect паттерн
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function loadUser() {
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal
});
const data = await response.json();
setUser(data);
} catch (error) {
if (error.name !== 'AbortError') {
console.error(error);
}
}
}
loadUser;
// Cleanup: отменить при размонтировании или смене userId
return => controller.abort();
}, [userId]);
return user ? <div>{user.name}</div> : <div>Loading...</div>;
}
AbortSignal.any (ES2024)
// Комбинация нескольких сигналов
const userController = new AbortController();
const timeoutSignal = AbortSignal.timeout(10000);
const combinedSignal = AbortSignal.any([
userController.signal, // Ручная отмена
timeoutSignal // Автоматический таймаут
]);
fetch('/api/data', { signal: combinedSignal });
// Отменится при любом из условий:
// - userController.abort()
// - через 10 секунд
Частые ошибки
1. Не проверять AbortError
// Плохо: AbortError показывается как ошибка
fetch(url, { signal }).catch(err => {
showErrorToUser(err.message); // Показывает "The operation was aborted"
});
// Хорошо: игнорировать AbortError
fetch(url, { signal }).catch(err => {
if (err.name !== 'AbortError') {
showErrorToUser(err.message);
}
});
2. Повторное использование контроллера
const controller = new AbortController();
controller.abort(); // Отменён навсегда
// Новый fetch с тем же signal — мгновенно отменится!
fetch('/api', { signal: controller.signal }); // Сразу AbortError
// Создавай новый контроллер для каждой операции
Практика
- Реализуй fetch с таймаутом через AbortController
- Сделай поисковые подсказки с отменой предыдущего запроса
- Используй AbortController для очистки нескольких event listeners
- Комбинируй ручную отмену и таймаут через AbortSignal.any
Связанные темы
Ресурсы
🎓 Источник: Отмена асинхронных операций
- 📅 2019-05-02 · YouTube
- Тезисы:
- Мы отменяем НЕ операцию, а интерес к её результату.
- До AbortController использовали ручные обёртки: третий параметр
onCancelв executor'е, обёртки над XHR сabort. - Наследование
class extends Promiseдля cancel — антипаттерн.
- См. Cancellable Promise для подробностей.
⚡ Источник: LeetCode — Design Cancellable Function · AsForJS
- 📅 2023-11-28 · YouTube
- Тезисы:
- Отмена generator-based async через
throwв генератор. - В реальной задаче собственный сигнал отмены реализуется как
Promise.race([task, cancelSignal]).
- Отмена generator-based async через
🎓 Источник: JavaScript собеседование — асинхронность
- 📅 2024-06-29 · YouTube
- Тезис:
AbortSignal.timeout(ms)— нативный таймаут, не требует ручного clearTimeout.AbortSignal.any([s1, s2])— комбинирует несколько сигналов.