Abort Controller: отмена запросов

AbortController — браузерный API для отмены асинхронных операций: fetch-запросов, потоков чтения и других задач, поддерживающих AbortSignal.

Зачем нужно

Без отмены запросов быстрые действия пользователя (смена маршрута, повторный поиск) порождают «гонку ответов»: старый запрос может прийти позже нового и перезаписать актуальные данные. AbortController позволяет отменить устаревший запрос до получения ответа, что снижает нагрузку на сеть и устраняет баги состояния гонки.

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

  • Живой поиск (debounce + abort предыдущего запроса при новом вводе)
  • React/Vue компоненты — отмена запроса при размонтировании компонента
  • Пагинация — отмена предыдущей страницы при переходе на следующую
  • Таймаут запроса без нативной поддержки в fetch

Основы AbortController

// Создаём контроллер и получаем его сигнал
const controller = new AbortController();
const { signal } = controller;

// Передаём сигнал в fetch
fetch('/api/search?q=query', { signal })
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Запрос отменён');  // ожидаемая ситуация
    } else {
      console.error('Сетевая ошибка:', err);
    }
  });

// Отменяем запрос
controller.abort();
// AbortError брошен — промис fetch отклонён

Живой поиск с отменой

let controller = null;

async function search(query) {
  // Отменяем предыдущий запрос, если он ещё не завершился
  if (controller) {
    controller.abort();
  }
  controller = new AbortController();

  try {
    const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
      signal: controller.signal,
    });
    const data = await res.json();
    renderResults(data);
  } catch (err) {
    if (err.name !== 'AbortError') throw err;
  }
}

document.querySelector('#search').addEventListener('input', e => {
  search(e.target.value);
});

Отмена в React useEffect

function SearchResults({ query }) {
  const [results, setResults] = React.useState();

  React.useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/search?q=${query}`, { signal: controller.signal })
      .then(res => res.json())
      .then(data => setResults(data))
      .catch(err => {
        if (err.name !== 'AbortError') console.error(err);
      });

    // cleanup — отменяем при смене query или размонтировании
    return  => controller.abort();
  }, [query]);

  return <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>;
}

Таймаут через AbortController

async function fetchWithTimeout(url, ms = 5000) {
  const controller = new AbortController();
  const timerId = setTimeout( => controller.abort(), ms);

  try {
    const res = await fetch(url, { signal: controller.signal });
    return await res.json();
  } finally {
    clearTimeout(timerId);
  }
}

// Начиная с Node 18 / Chrome 124 есть AbortSignal.timeout:
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });

Частые ошибки

  • Не проверяют err.name === 'AbortError' — обрабатывают отмену как ошибку сети
  • Создают новый AbortController внутри обработчика события без отмены предыдущего
  • Забывают вернуть cleanup-функцию из useEffect — запрос продолжает работать после размонтирования
  • Вызывают controller.abort() после получения ответа — бесполезно, сигнал уже не нужен

Связанные темы

Ресурсы