


Чтобы отменить fetch, создайте AbortController, передайте его signal в запрос и вызовите abort(), когда результат больше не нужен. В поисковой строке этого мало: обновлять данные, ошибку и индикатор загрузки должен только последний активный запрос.
Разбор Mentoring.digital посвящён именно этой гонке. Код использует обычный JavaScript и не зависит от React или другого фреймворка.
Пользователь ввёл первый поисковый запрос, затем сразу уточнил его. Второй ответ может прийти раньше первого. Если оба обработчика без проверки записывают данные в интерфейс, старый ответ заменит более актуальный.
Отмена помогает прекратить ненужное ожидание и чтение ответа на клиенте. Но она не гарантирует отмену уже начавшейся работы сервера. Поэтому такой приём подходит для загрузки данных, а для операции вроде создания заказа нужны отдельные правила обработки повторов и результата операции.
Есть ещё менее заметная ошибка: старый запрос завершает свой блок finally и скрывает индикатор загрузки нового. Защищать нужно все изменения состояния, а не только отрисовку списка.
const controller = new AbortController();
const response = await fetch("/api/search", {
signal: controller.signal
});
// Из другого обработчика, пока запрос выполняется:
// controller.abort();/api/search здесь обозначает маршрут вашего приложения, который нужно заменить существующим адресом. Вызов abort() при отмене по умолчанию приводит к отклонению связанной операции с AbortError. Отмена также может произойти во время чтения тела ответа, поэтому response.json() тоже должен находиться внутри try.
Один сигнал нельзя «включить обратно». Новый запрос получает новый контроллер. Если передать уже отменённый сигнал повторно, запрос сразу отклонится.
Ниже фабрика возвращает функцию search. Обработчики onData, onError и onLoading предоставляются приложением: они обновляют интерфейс. Параметр fetchImpl по умолчанию использует встроенный fetch; при тестировании его можно заменить.
export function createSearch({
fetchImpl = fetch, onData, onError, onLoading
}) {
let active = null;
return async function search(url) {
active?.abort();
const controller = new AbortController();
active = controller;
onLoading(true);
try {
const response = await fetchImpl(url, {
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (active === controller) onData(data);
} catch (error) {
if (!controller.signal.aborted &&
active === controller) {
onError(error);
}
} finally {
if (active === controller) {
active = null;
onLoading(false);
}
}
};
}Переменная active хранит контроллер последнего вызова. Перед новым запросом старый контроллер получает отмену. Проверка active === controller не позволяет старому вызову перезаписать данные или закончить загрузку за новый.
В catch отдельно проверяется сигнал данного запроса. Ожидаемая отмена не показывается как ошибка пользователю. При этом HTTP 500 не считается успешным ответом: fetch сам по себе не отклоняет Promise из-за такого статуса, поэтому код явно проверяет response.ok.
Создавайте функцию один раз для одного независимого блока результатов. В следующем фрагменте renderResults, showError и setLoading обозначают ваши функции отображения:
const search = createSearch({
onData: renderResults,
onError: showError,
onLoading: setLoading
});
function onSearchInput(query) {
const params = new URLSearchParams({ q: query });
void search("/api/search?" + params);
}Если создавать новую фабрику на каждый ввод символа, её active каждый раз будет пустым и предыдущий запрос не отменится. Если же использовать один экземпляр для двух независимых виджетов, они начнут отменять запросы друг друга.
Этот пример решает замену поискового запроса. Для компонента, который может исчезнуть со страницы, дополнительно нужен доступный извне метод отмены и его вызов при очистке компонента. Не считайте этот фрагмент готовой реализацией всего жизненного цикла приложения.
Мы запустили пример в Node.js 24.19.0 со встроенным fetch и локальным HTTP-сервером. Сервер задерживал ответ на первый запрос и быстро отвечал на второй. Проверки подтвердили:
Это локальные проверки поведения, а не замер скорости браузера. В вашем интерфейсе дополнительно проверьте последовательность «ввод, новый ввод, очистка поля», исчезновение компонента и реальный формат JSON. В обработчиках отображения не должно быть своих необработанных исключений.
Debounce задерживает начало нового запроса и помогает уменьшить их число. AbortController отменяет уже начатую операцию. Проверка текущего контроллера определяет, чей результат можно применять. У каждого механизма своя задача; ни один из них не делает серверную операцию автоматически обратимой.
Не скрывайте все ошибки общим пустым catch: тогда вместе с отменой исчезнут сетевые сбои и неверный JSON. И не добавляйте бесконечный повтор запроса при отмене, иначе код сам будет создавать ненужную работу.
Для разбора похожей гонки удобно принести ментору по JavaScript небольшой пример с двумя запросами и журналом порядка событий. Так можно проверить не только синтаксис API, но и то, кому принадлежит состояние интерфейса.