Клавиша / 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('Запрос был отменён')    }  })// Отменяем запрос через 2 секундыsetTimeout(() => {  controller.abort()}, 2000)
          // Создаём контроллер
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('Запрос был отменён')
    }
  })

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

        
        
          
        
      

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

Скопировано
        
          
          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({ name: 'AbortError', message: 'Операция устарела' })// В обработчике ошибкиtry {  ...} catch (error) {  if (error.name === 'AbortError') {    console.log(error.message) // "Операция устарела"  }}
          controller.abort({ name: 'AbortError', message: 'Операция устарела' })

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

        
        
          
        
      

Если передать в качестве reason строку, а не объект, то и в обработчик ошибок попадёт строка:

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

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

        
        
          
        
      

Использование 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) // "Операция устарела"}
          controller.abort('Операция устарела')

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

        
        
          
        
      

Это более декларативный способ проверки состояния сигнала по сравнению с проверкой 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({ name: 'AbortError', message:'Отмена через первый контроллер'})  controller2.abort({ name: 'AbortError', message:'Отмена через второй контроллер'})
          // Создаем два контроллера
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({ name: 'AbortError', message:'Отмена через первый контроллер'})
  controller2.abort({ name: 'AbortError', message:'Отмена через второй контроллер'})

        
        
          
        
      

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

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

Скопировано

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

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

// Используем сигнал для запроса
fetch(url, { signal })
  .then((response) => response.json())
  .catch((error) => {
    if (error.name === 'TimeoutError') {
      // При таймауте 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()
  }, [])
}