По умолчанию у fetch нет ни таймаута, ни кнопки «отмени». Запрос ушёл — и висит, пока сервер не ответит или браузер не сдастся (а сдастся он минут через пять). Для боевого фронта это никуда не годится. Решает обе задачи одна штука — AbortController.

Зачем отменять запросы

Классический случай — автокомплит в поисковой строке. Пользователь печатает «f» — ушёл запрос. Печатает «fr» — ушёл второй. «fru» — третий. «frun» — четвёртый. К моменту, когда пользователь дописал «fruntend», по сети летят восемь параллельных запросов, и ответы могут вернуться в произвольном порядке.

Если каждый ответ пишет результаты в UI, может произойти неприятное: ответ на старый «f» пришёл последним — и переписал результаты для свежего «fruntend». Пользователь видит подсказки на букву «f» и недоумевает.

Правильное решение — отменять предыдущий запрос, когда стартует новый. Чтобы старые ответы вообще не возвращались.

Базовый AbortController

const controller = new AbortController();

fetch('/api/search?q=fruntend', { signal: controller.signal })
  .then(response => response.json())
  .then(render)
  .catch(error => {
    if (error.name === 'AbortError') return;   // запрос мы сами отменили — это норма
    throw error;
  });

// Где-то позже, например при следующем нажатии клавиши:
controller.abort();

Что происходит. Создали AbortController — у него есть свойство .signal и метод .abort(). Передали signal в fetch. Когда зовём controller.abort() — запрос отменяется, Promise отвергается с AbortError. В catch такую ошибку отличаем по error.name и игнорируем (это не настоящая ошибка — мы сами попросили).

Полный пример с автокомплитом

let currentController = null;

input.addEventListener('input', async () => {
  // Отменяем предыдущий запрос, если он ещё жив:
  if (currentController) currentController.abort();
  currentController = new AbortController();

  try {
    const params = new URLSearchParams({ q: input.value });
    const response = await fetch(`/api/search?${params}`, {
      signal: currentController.signal,
    });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const items = await response.json();
    renderResults(items);
  } catch (error) {
    if (error.name !== 'AbortError') showError(error.message);
  }
});

Теперь, сколько бы букв пользователь ни набил, в UI попадёт результат только последнего запроса. Остальные молча оборвутся.

Таймаут

Тот же AbortController делает и таймаут. Просто запускаем setTimeout, который через N миллисекунд позовёт .abort():

function fetchWithTimeout(url, options = {}, ms = 5000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);

  return fetch(url, { ...options, signal: controller.signal })
    .finally(() => clearTimeout(timer));
}

// Использование:
try {
  const response = await fetchWithTimeout('/api/slow', {}, 3000);
  // ...
} catch (error) {
  if (error.name === 'AbortError') {
    showError('Сервер не отвечает дольше 3 секунд');
  } else {
    showError('Ошибка сети: ' + error.message);
  }
}

Короче: AbortSignal.timeout

В современных браузерах есть встроенный сахар — не нужно собирать setTimeout руками:

fetch('/api/slow', {
  signal: AbortSignal.timeout(3000),  // отменить через 3 секунды
});

Если нужно одновременно и таймаут, и ручная отмена — есть AbortSignal.any([signal1, signal2]): сработает, как только сработает любой из них.

const userController = new AbortController();

fetch(url, {
  signal: AbortSignal.any([
    userController.signal,
    AbortSignal.timeout(5000),
  ]),
});

AbortController сам работает с 2017 года везде. AbortSignal.timeout и AbortSignal.any появились позже — в безопасных версиях с примерно 2022 года.

Поддержка браузерами
chrome
Chrome
66
firefox
Firefox
57
edge
Edge
16
safari
Safari
11.1
opera
Opera
53

Когда отменять обязательно

  • Автокомплит, поиск, фильтры с задержкой.
  • Размонтирование компонента, который сделал запрос (React, Vue) — чтобы не пытаться setState на удалённом компоненте.
  • Долгие запросы, которые пользователь может захотеть прервать (большая выгрузка, AI-генерация).
  • Любые запросы без таймаута, если за SLA сервера ручаться нельзя.