AbortController

AbortController — интерфейс для отмены асинхронных операций (fetch-запросов, event listeners, потоков).

Зачем нужно

Пользователь переключил страницу, но запрос ещё летит. Без отмены: ответ придёт и обновит уже ненужный UI, потратит трафик и может вызвать ошибки. AbortController позволяет отменить операцию чисто.

Где используется

  • Отмена fetch-запросов при навигации
  • Таймаут для сетевых операций
  • Очистка event listeners
  • Отмена при перезапросе (поисковые подсказки)
  • React: отмена в useEffect cleanup

Предпосылки

Fetch API, Promise

Основной 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

// Создавай новый контроллер для каждой операции

Практика

  1. Реализуй fetch с таймаутом через AbortController
  2. Сделай поисковые подсказки с отменой предыдущего запроса
  3. Используй AbortController для очистки нескольких event listeners
  4. Комбинируй ручную отмену и таймаут через 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]).

🎓 Источник: JavaScript собеседование — асинхронность

  • 📅 2024-06-29 · YouTube
  • Тезис: AbortSignal.timeout(ms) — нативный таймаут, не требует ручного clearTimeout. AbortSignal.any([s1, s2]) — комбинирует несколько сигналов.