Отмена HTTP-запросов в полёте с помощью AbortController
  Современные веб-приложения часто выполняют HTTP-запросы, которые пользователям может не потребоваться завершать. Пользователь, вводящий текст в поисковое поле, генерирует множество запросов, но важен только последний. Пользователь, переходящий с одной страницы на другую, делает все ожидающие запросы для этой страницы неактуальными. Без правильной отмены эти ненужные запросы тратят пропускную способность, потребляют ресурсы сервера и могут привести к появлению устаревших данных в пользовательском интерфейсе.
API AbortController предоставляет чистый, стандартизированный способ отмены fetch-запросов и других асинхронных операций. В этой статье показано, как реализовать отмену запросов с использованием AbortController, включая практические паттерны, такие как debouncing поиска, очистка компонентов и обработка таймаутов.
Ключевые выводы
- AbortController создаёт сигнал, который fetch отслеживает на предмет событий отмены
 - Всегда проверяйте AbortError в блоках catch, чтобы отличить отмены от сбоев
 - Отменяйте предыдущие запросы в интерфейсах поиска для предотвращения состояний гонки
 - Используйте функции очистки в компонентах фреймворков для отмены запросов при размонтировании
 - Комбинируйте AbortController с setTimeout для функциональности таймаута запросов
 - Каждый AbortController используется однократно - создавайте новые для логики повторных попыток
 - Node.js 18+ включает нативную поддержку AbortController
 
Что делают AbortController и AbortSignal
AbortController создаёт объект-контроллер, который управляет отменой через связанный AbortSignal. У контроллера одна задача: вызов abort(), когда вы хотите отменить операцию. Сигнал выступает в качестве канала связи, который fetch и другие API отслеживают на предмет событий отмены.
const controller = new AbortController()
const signal = controller.signal
// Сигнал изначально не прерван
console.log(signal.aborted) // false
// Вызов abort() изменяет состояние сигнала
controller.abort()
console.log(signal.aborted) // true
Когда вы передаёте сигнал в fetch и позже вызываете abort(), промис fetch отклоняется с DOMException с именем AbortError. Это позволяет различать отмены и реальные сетевые сбои.
Базовая реализация AbortController
Для отмены fetch-запроса создайте AbortController, передайте его сигнал в fetch, затем вызовите abort() при необходимости:
const controller = new AbortController()
fetch('/api/data', { signal: controller.signal })
  .then(response => response.json())
  .then(data => console.log('Data received:', data))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Request was cancelled')
    } else {
      console.error('Request failed:', error)
    }
  })
// Отменить запрос
controller.abort()
Ключевые моменты:
- Создайте контроллер перед выполнением запроса
 - Передайте 
controller.signalв опции fetch - Вызовите 
controller.abort()для отмены - Проверьте 
AbortErrorв блоке catch 
Как отменить поисковые запросы при вводе пользователем
Интерфейсы поиска часто запускают запросы при каждом нажатии клавиши. Без отмены медленные ответы могут приходить в неправильном порядке, показывая результаты для устаревших запросов. Вот как отменить предыдущие поисковые запросы:
let searchController = null
function performSearch(query) {
  // Отменить любой существующий поиск
  if (searchController) {
    searchController.abort()
  }
  // Создать новый контроллер для этого поиска
  searchController = new AbortController()
  fetch(`/api/search?q=${encodeURIComponent(query)}`, {
    signal: searchController.signal
  })
    .then(response => response.json())
    .then(results => {
      console.log('Search results:', results)
      updateSearchUI(results)
    })
    .catch(error => {
      if (error.name === 'AbortError') {
        console.log('Search cancelled')
      } else {
        console.error('Search failed:', error)
        showSearchError()
      }
    })
}
// Использование: каждый новый поиск отменяет предыдущий
performSearch('javascript')
performSearch('javascript frameworks') // Отменяет поиск 'javascript'
Этот паттерн гарантирует, что в пользовательском интерфейсе появляются только результаты самого последнего поиска, предотвращая состояния гонки, когда более медленные запросы перезаписывают более быстрые.
Отмена запросов при размонтировании компонента
Во фронтенд-фреймворках компоненты часто выполняют fetch-запросы, которые должны быть отменены при размонтировании компонента. Это предотвращает утечки памяти и ошибки от попыток обновления размонтированных компонентов.
Пример для React
import { useEffect, useState } from 'react'
function UserProfile({ userId }) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
  useEffect(() => {
    const controller = new AbortController()
    async function fetchUser() {
      try {
        const response = await fetch(`/api/users/${userId}`, {
          signal: controller.signal
        })
        
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`)
        }
        
        const userData = await response.json()
        setUser(userData)
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('Failed to fetch user:', error)
        }
      } finally {
        if (!controller.signal.aborted) {
          setLoading(false)
        }
      }
    }
    fetchUser()
    // Функция очистки отменяет запрос
    return () => controller.abort()
  }, [userId])
  if (loading) return <div>Loading...</div>
  return <div>{user?.name}</div>
}
Пример для Vue
export default {
  data() {
    return {
      user: null,
      controller: null
    }
  },
  async mounted() {
    this.controller = new AbortController()
    
    try {
      const response = await fetch(`/api/users/${this.userId}`, {
        signal: this.controller.signal
      })
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }
      
      this.user = await response.json()
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('Failed to fetch user:', error)
      }
    }
  },
  beforeUnmount() {
    if (this.controller) {
      this.controller.abort()
    }
  }
}
Реализация таймаутов запросов с AbortController
Сетевые запросы могут зависать бесконечно. AbortController в сочетании с setTimeout предоставляет чистый механизм таймаута:
function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
  const controller = new AbortController()
  
  const timeoutId = setTimeout(() => {
    controller.abort()
  }, timeoutMs)
  return fetch(url, {
    ...options,
    signal: controller.signal
  }).finally(() => {
    clearTimeout(timeoutId)
  })
}
// Использование
fetchWithTimeout('/api/slow-endpoint', {}, 3000)
  .then(response => response.json())
  .then(data => console.log('Data:', data))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Request timed out')
    } else {
      console.error('Request failed:', error)
    }
  })
Для переиспользуемой логики таймаута создайте утилитарную функцию:
function createTimeoutSignal(timeoutMs) {
  const controller = new AbortController()
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
  
  // Очистить таймаут при использовании сигнала
  controller.signal.addEventListener('abort', () => {
    clearTimeout(timeoutId)
  }, { once: true })
  
  return controller.signal
}
// Использование
fetch('/api/data', { signal: createTimeoutSignal(5000) })
  .then(response => response.json())
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Request timed out or was cancelled')
    }
  })
Правильная обработка ошибок для отменённых запросов
Всегда различайте отмены и реальные ошибки. AbortError указывает на намеренную отмену, а не на сбой:
async function handleRequest(url) {
  const controller = new AbortController()
  
  try {
    const response = await fetch(url, { signal: controller.signal })
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
    }
    
    return await response.json()
  } catch (error) {
    if (error.name === 'AbortError') {
      // Это ожидается при отмене - не логировать как ошибку
      console.log('Request was cancelled')
      return null
    }
    
    // Это реальная ошибка, которая требует обработки
    console.error('Request failed:', error)
    throw error
  }
}
Для приложений с отслеживанием ошибок исключите AbortError из отчётов об ошибках:
.catch(error => {
  if (error.name === 'AbortError') {
    // Не сообщать об отменах в отслеживание ошибок
    return
  }
  
  // Сообщить о реальных ошибках
  errorTracker.captureException(error)
  throw error
})
Управление множественными запросами
При работе с несколькими параллельными запросами вам может потребоваться отменить их все сразу или управлять ими индивидуально:
class RequestManager {
  constructor() {
    this.controllers = new Map()
  }
  
  async fetch(key, url, options = {}) {
    // Отменить существующий запрос с тем же ключом
    this.cancel(key)
    
    const controller = new AbortController()
    this.controllers.set(key, controller)
    
    try {
      const response = await fetch(url, {
        ...options,
        signal: controller.signal
      })
      return response
    } finally {
      this.controllers.delete(key)
    }
  }
  
  cancel(key) {
    const controller = this.controllers.get(key)
    if (controller) {
      controller.abort()
      this.controllers.delete(key)
    }
  }
  
  cancelAll() {
    for (const controller of this.controllers.values()) {
      controller.abort()
    }
    this.controllers.clear()
  }
}
// Использование
const requestManager = new RequestManager()
// Эти запросы могут управляться независимо
requestManager.fetch('user-profile', '/api/user/123')
requestManager.fetch('user-posts', '/api/user/123/posts')
// Отменить конкретный запрос
requestManager.cancel('user-profile')
// Отменить все ожидающие запросы
requestManager.cancelAll()
Поддержка браузерами и совместимость с Node.js
AbortController имеет отличную поддержку браузерами:
- Chrome 66+
 - Firefox 57+
 - Safari 12.1+
 - Edge 16+
 
Для Node.js AbortController доступен нативно в Node 18+. Для более ранних версий используйте полифилл abort-controller:
npm install abort-controller
// Для Node.js < 18
const { AbortController } = require('abort-controller')
// Использовать как обычно
const controller = new AbortController()
Заключение
AbortController предоставляет чистый, стандартизированный способ отмены fetch-запросов в современных веб-приложениях. Ключевые паттерны: создать контроллер перед каждым запросом, передать его сигнал в fetch и вызвать abort() при необходимости отмены. Всегда обрабатывайте AbortError отдельно от реальных сетевых сбоев, чтобы не рассматривать намеренные отмены как ошибки.
Наиболее распространённые случаи использования — управление поисковыми запросами, очистка компонентов и обработка таймаутов — следуют простым паттернам, которые вы можете адаптировать под свои конкретные потребности. При правильной реализации отмена запросов улучшает как пользовательский опыт, так и производительность приложения, предотвращая ненужную сетевую активность и обновления устаревшими данными.
Готовы реализовать отмену запросов в своём приложении? Начните с базового паттерна для наиболее распространённого случая использования, затем расширьте до более сложных сценариев по мере необходимости. Инвестиции в правильное управление запросами окупаются в производительности приложения и пользовательском опыте.
Часто задаваемые вопросы
Нет, после вызова abort() сигнал контроллера остаётся в прерванном состоянии навсегда. Вы должны создать новый AbortController для каждого нового запроса или попытки повтора.
Нет, abort() только предотвращает обработку ответа вашим JavaScript-кодом. Если запрос уже был отправлен по сети, сервер всё ещё может получить и обработать его.
Создайте несколько контроллеров и сохраните их в массиве или Map, затем вызовите abort() для каждого. Альтернативно, используйте один контроллер и передайте его сигнал нескольким fetch-вызовам - однократный вызов abort() отменит их все.
Ничего не происходит. Вызов abort() игнорируется, если запрос уже успешно завершился или завершился с ошибкой по другим причинам.
Да, AbortController работает одинаково как с цепочками Promise, так и с async/await. Промис fetch будет отклонён с AbortError при отмене, что можно поймать блоками try/catch.