Клавиша / esc

AbortController

Встроенный объект для отмены асинхронных операций и не только.

Время чтения: 6 мин

Кратко

Скопировано

AbortController — это встроенный объект, который позволяет отменять выполнение любых операций. Появился в ES2018 (ES9) для отмены fetch запросов, но позже его применение расширилось на другие операции.

Как понять

Скопировано

AbortController - это механизм для отмены операций. С его помощью можно:

  • отменять fetch запросы
  • удалять обработчики событий
  • останавливать стримы
  • прерывать любые другие операции

Состоит из:

  1. Метода abort([reason]) для отмены операции, где reason - необязательный параметр.

При вызове метода abort([reason]) reason будет доступен через signal.reason. В reason можно передать любое значение: строку, число, объект, ошибку и т.д.

  1. Свойства signal, возвращает объект, который является экземпляром AbortSignal со следующими свойствами и методами:
  • aborted - булево значение, указывающее было ли выполнено прерывание;
  • reason - причина отмены;
  • onabort - обработчик события отмены;
  • throwIfAborted() - выбрасывает ошибку с причиной отмены, если сигнал в состоянии "отменён".

При отмене операций чаще всего возникает ошибка типа "AbortError". Она появляется в трёх случаях:

  • Не передан reason в abort();
  • При использовании встроенных API (например fetch), которые сами создают AbortError;
  • При создании через new DOMException() с именем "AbortError".

В остальных случаях тип ошибки будет зависеть от того, что было передано в reason.

Также у AbortSignal есть статические методы:

  • AbortSignal.abort([reason]) - создает уже отмененный сигнал;
  • AbortSignal.timeout(milliseconds) - создает сигнал, который будет отменен через указанное время;
  • AbortSignal.any(signals) - создает сигнал, который будет отменен, если хотя бы один из переданных сигналов отменен.

Статические методы используются в случаях, когда не нужен контроллер для ручной отмены.

Как пишется

Скопировано
        
          
          // Создаём контроллерconst controller = new AbortController()const API_URL = 'https://jsonplaceholder.typicode.com'// Делаем запрос с сигналомfetch(`${API_URL}/posts/1`, { signal: controller.signal })  .then((response) => response.json())  .catch((error) => {    if (error.name === 'AbortError') {      console.log('Запрос был отменён')    }  })// Отменяем запрос через 5 секундsetTimeout(() => {  controller.abort()}, 5000)
          // Создаём контроллер
const controller = new AbortController()
const API_URL = 'https://jsonplaceholder.typicode.com'

// Делаем запрос с сигналом
fetch(`${API_URL}/posts/1`, { signal: controller.signal })
  .then((response) => response.json())
  .catch((error) => {
    if (error.name === 'AbortError') {
      console.log('Запрос был отменён')
    }
  })

// Отменяем запрос через 5 секунд
setTimeout(() => {
  controller.abort()
}, 5000)

        
        
          
        
      

Использование с событиями

Скопировано
        
          
          const controller = new AbortController()const { signal } = controllerconst handler = () => console.log('Клик!')// Добавляем обработчик с сигналомelement.addEventListener('click', handler, { signal })// Удаляем обработчик через AbortControllercontroller.abort()// Это аналогично удалению через removeEventListener:element.addEventListener('click', handler)element.removeEventListener('click', handler)
          const controller = new AbortController()
const { signal } = controller

const handler = () => console.log('Клик!')

// Добавляем обработчик с сигналом
element.addEventListener('click', handler, { signal })

// Удаляем обработчик через AbortController
controller.abort()

// Это аналогично удалению через removeEventListener:
element.addEventListener('click', handler)
element.removeEventListener('click', handler)

        
        
          
        
      

Отмена нескольких операций

Скопировано

Один сигнал можно использовать для отмены нескольких операций:

        
          
          const controller = new AbortController()const { signal } = controllerconst API_URL = 'https://jsonplaceholder.typicode.com'// Запускаем несколько запросовPromise.all([  fetch(`${API_URL}/posts/1`, { signal }),  fetch(`${API_URL}/posts/2`, { signal }),  fetch(`${API_URL}/posts/3`, { signal }),]).catch((error) => {  if (error.name === 'AbortError') {    console.log('Все запросы отменены')  }})// Отменяем все запросы одной командойcontroller.abort()
          const controller = new AbortController()
const { signal } = controller
const API_URL = 'https://jsonplaceholder.typicode.com'

// Запускаем несколько запросов
Promise.all([
  fetch(`${API_URL}/posts/1`, { signal }),
  fetch(`${API_URL}/posts/2`, { signal }),
  fetch(`${API_URL}/posts/3`, { signal }),
]).catch((error) => {
  if (error.name === 'AbortError') {
    console.log('Все запросы отменены')
  }
})

// Отменяем все запросы одной командой
controller.abort()

        
        
          
        
      

Передача причины отмены

Скопировано

Можно указать причину отмены, передав её в метод abort():

        
          
          controller.abort('Операция устарела')// В обработчике ошибкиtry {  ...} catch (error) {  if (error.name === 'AbortError') {    console.log(error.message) // "Операция устарела"  }}
          controller.abort('Операция устарела')

// В обработчике ошибки
try {
  ...
} catch (error) {
  if (error.name === 'AbortError') {
    console.log(error.message) // "Операция устарела"
  }
}

        
        
          
        
      

Использование onabort

Скопировано

onabort - это свойство для быстрого назначения обработчика события отмены:

        
          
          // Через onabort - быстро и простоsignal.onabort = () => {  console.log('Операция отменена')  console.log('Причина:', signal.reason)}// Через addEventListener - больше кодаconst handler = () => {  console.log('Операция отменена')  console.log('Причина:', signal.reason)}signal.addEventListener('abort', handler)
          // Через onabort - быстро и просто
signal.onabort = () => {
  console.log('Операция отменена')
  console.log('Причина:', signal.reason)
}

// Через addEventListener - больше кода
const handler = () => {
  console.log('Операция отменена')
  console.log('Причина:', signal.reason)
}
signal.addEventListener('abort', handler)

        
        
          
        
      

Плюсы:

  • Простой способ узнать момент отмены операции;
  • Лаконичный синтаксис;
  • Не нужно хранить ссылку на функцию-обработчик.

Минусы:

  • Можно установить только один обработчик;
  • При повторном присвоении предыдущий обработчик теряется;
  • Нет прямого способа удалить обработчик (только присвоить null).

Использование throwIfAborted()

Скопировано

Метод throwIfAborted() полезен для проверки состояния сигнала - он выбросит ошибку, если сигнал находится в состоянии "отменён":

        
          
          controller.abort('Операция устарела')try {  // Проверяем состояние сигнала  signal.throwIfAborted()  // Этот код не выполнится, если сигнал отменён  await someAsyncOperation()} catch (error) {  console.log(error.message) // "Операция устарела"}
          controller.abort('Операция устарела')

try {
  // Проверяем состояние сигнала
  signal.throwIfAborted()
  // Этот код не выполнится, если сигнал отменён
  await someAsyncOperation()
} catch (error) {
  console.log(error.message) // "Операция устарела"
}

        
        
          
        
      

Это более декларативный способ проверки состояния сигнала по сравнению с проверкой signal.aborted.

Использование AbortSignal.any()

Скопировано

AbortSignal.any() создает сигнал, который будет отменен, если хотя бы один из переданных сигналов отменен:

        
          
          // Создаем два контроллераconst controller1 = new AbortController()const controller2 = new AbortController()// Создаем сигнал, который сработает при отмене любого из контроллеровconst signal = AbortSignal.any([controller1.signal, controller2.signal])// Используем общий сигнал для запросаfetch(url, { signal })  .then((response) => response.json())  .catch((error) => {    if (error.name === 'AbortError') {      console.log('Запрос отменен:', error.message)    }  })// Отмена любого из контроллеров приведет к отмене запросаcontroller1.abort('Отмена через первый контроллер')controller2.abort('Отмена через второй контроллер')
          // Создаем два контроллера
const controller1 = new AbortController()
const controller2 = new AbortController()

// Создаем сигнал, который сработает при отмене любого из контроллеров
const signal = AbortSignal.any([controller1.signal, controller2.signal])

// Используем общий сигнал для запроса
fetch(url, { signal })
  .then((response) => response.json())
  .catch((error) => {
    if (error.name === 'AbortError') {
      console.log('Запрос отменен:', error.message)
    }
  })

// Отмена любого из контроллеров приведет к отмене запроса
controller1.abort('Отмена через первый контроллер')
controller2.abort('Отмена через второй контроллер')

        
        
          
        
      

Это полезно для отмены нескольких операций, которые могут быть отменены независимо друг от друга.

Использование AbortSignal.timeout()

Скопировано

AbortSignal.timeout() создает сигнал, который будет автоматически отменен через указанное количество миллисекунд:

        
          
          // Создаем сигнал с таймаутом в 5 секундconst signal = AbortSignal.timeout(5000)// Используем сигнал для запросаfetch(url, { signal })  .then((response) => response.json())  .catch((error) => {    if (error.name === 'AbortError') {      // При таймауте reason будет установлен как "TimeoutError" DOMException      console.log('Запрос отменен по таймауту:', error.message)    }  })
          // Создаем сигнал с таймаутом в 5 секунд
const signal = AbortSignal.timeout(5000)

// Используем сигнал для запроса
fetch(url, { signal })
  .then((response) => response.json())
  .catch((error) => {
    if (error.name === 'AbortError') {
      // При таймауте reason будет установлен как "TimeoutError" DOMException
      console.log('Запрос отменен по таймауту:', error.message)
    }
  })

        
        
          
        
      

Это полезно для отмены долгих операций, которые могут занять больше времени, чем ожидалось. Удобная альтернатива ручной установке таймера с setTimeout() и созданию AbortController.

Подсказки

Скопировано

💡 Создавайте новый контроллер для каждой группы связанных операций. После вызова abort() сигнал остаётся в состоянии "отменён", поэтому для новых операций нужно создать новый контроллер. Не используйте один контроллер для всего приложения.

💡 Метод abort() нужно вызывать только в контексте контроллера: controller.abort(). Деструктуризация метода приведёт к потере контекста.

Поддержка в браузерах:
  • Chrome 66, поддерживается
  • Edge 16, поддерживается
  • Firefox 57, поддерживается
  • Safari 12.1, поддерживается
О Baseline

На практике

Скопировано

Игорь Теплостанский советует

Скопировано

🛠 AbortController упрощает отмену асинхронных запросов в React-компоненте. Это особенно полезно при использовании React.StrictMode, чтобы избежать лишних запросов к серверу, так как StrictMode в development режиме запускает дополнительный цикл установки и сброса useEffect.

        
          
          function SearchComponent() {  const [search, setSearch] = useState('')  const API_URL = 'https://jsonplaceholder.typicode.com'  useEffect(() => {    const controller = new AbortController()    // Запрос отменится при новом поиске или размонтировании    fetch(`${API_URL}/posts?userId=${search}`, { signal: controller.signal })      .then(response => response.json())      .then(data => console.log('Результаты:', data))      .catch(error => {        if (error.name === 'AbortError') return        console.error(error)      })    // Очистка при размонтировании и ререндере    return () => controller.abort()  }, [search])  return (/* ... */)}
          function SearchComponent() {
  const [search, setSearch] = useState('')
  const API_URL = 'https://jsonplaceholder.typicode.com'

  useEffect(() => {
    const controller = new AbortController()

    // Запрос отменится при новом поиске или размонтировании
    fetch(`${API_URL}/posts?userId=${search}`, { signal: controller.signal })
      .then(response => response.json())
      .then(data => console.log('Результаты:', data))
      .catch(error => {
        if (error.name === 'AbortError') return
        console.error(error)
      })

    // Очистка при размонтировании и ререндере
    return () => controller.abort()
  }, [search])

  return (/* ... */)
}

        
        
          
        
      

Пример отписки от событий:

        
          
          function EventComponent() {  useEffect(() => {    const controller = new AbortController()    // Один сигнал для всех обработчиков    window.addEventListener('resize', onResize, { signal: controller.signal })    window.addEventListener('keydown', onKeyDown, { signal: controller.signal })    // Очистка при размонтировании    return () => controller.abort()  }, [])}
          function EventComponent() {
  useEffect(() => {
    const controller = new AbortController()
    // Один сигнал для всех обработчиков
    window.addEventListener('resize', onResize, { signal: controller.signal })
    window.addEventListener('keydown', onKeyDown, { signal: controller.signal })

    // Очистка при размонтировании
    return () => controller.abort()
  }, [])
}