Обработка ошибок

Работа с удалёнными данными всегда связана с потенциальными сбоями. Сервер может быть недоступен, сеть — нестабильной, API — вернуть некорректный ответ, а пользователь — потерять интернет-соединение во время выполнения запроса. TanStack Query предоставляет развитую систему обработки ошибок, позволяющую централизованно управлять состояниями сбоев, повторными попытками, уведомлениями и поведением интерфейса.

Внутри TanStack Query ошибка считается полноценным состоянием запроса наряду с загрузкой и успешным завершением. Любой запрос может находиться в одном из состояний:

  • pending
  • success
  • error

При возникновении исключения внутри queryFn запрос автоматически переходит в состояние ошибки.

Базовый пример:

import { useQuery } from '@tanstack/react-query'

function Users() {
  const {
    data,
    error,
    isError,
    isPending
  } = useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const response = await fetch('/api/users')

      if (!response.ok) {
        throw new Error('Ошибка загрузки пользователей')
      }

      return response.json()
    }
  })

  if (isPending) {
    return <div>Загрузка...</div>
  }

  if (isError) {
    return <div>{error.message}</div>
  }

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

Как TanStack Query определяет ошибку

TanStack Query не анализирует HTTP-статусы автоматически. Библиотека считает ошибкой только выброшенное исключение.

Это означает, что следующий код не вызовет ошибку:

queryFn: async () => {
  const response = await fetch('/api/users')

  return response.json()
}

Даже если сервер вернёт 404 или 500, fetch всё равно успешно завершится.

Правильный вариант:

queryFn: async () => {
  const response = await fetch('/api/users')

  if (!response.ok) {
    throw new Error(`HTTP Error: ${response.status}`)
  }

  return response.json()
}

Свойства ошибки

Хук useQuery возвращает несколько свойств, связанных с ошибками.

error

Содержит объект ошибки.

const { error } = useQuery(...)

Обычно это экземпляр Error.

console.log(error.message)

isError

Флаг, указывающий, что запрос завершился ошибкой.

const { isError } = useQuery(...)

status

Текстовое состояние запроса.

const { status } = useQuery(...)

Возможные значения:

pending
success
error

fetchStatus

Отражает физическое состояние сетевого запроса.

const { fetchStatus } = useQuery(...)

Возможные значения:

fetching
paused
idle

Иногда запрос имеет состояние error, но одновременно может повторно выполняться в фоне.


Обработка ошибок через try/catch

Внутри queryFn можно использовать полноценную обработку исключений.

queryFn: async () => {
  try {
    const response = await fetch('/api/users')

    if (!response.ok) {
      throw new Error('Ошибка API')
    }

    return response.json()
  } catch (error) {
    console.error(error)

    throw error
  }
}

Ключевой момент — ошибка должна быть повторно выброшена через throw, иначе TanStack Query посчитает запрос успешным.

Неправильный вариант:

queryFn: async () => {
  try {
    const response = await fetch('/api/users')

    return response.json()
  } catch (error) {
    console.error(error)
  }
}

В этом случае функция вернёт undefined, а запрос перейдёт в состояние success.


Пользовательские классы ошибок

Для сложных приложений полезно создавать собственные классы ошибок.

class ApiError extends Error {
  constructor(message, status) {
    super(message)

    this.status = status
  }
}

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

queryFn: async () => {
  const response = await fetch('/api/users')

  if (!response.ok) {
    throw new ApiError(
      'Ошибка загрузки',
      response.status
    )
  }

  return response.json()
}

В интерфейсе:

if (error.status === 404) {
  return <div>Данные не найдены</div>
}

if (error.status === 500) {
  return <div>Ошибка сервера</div>
}

Retry — автоматические повторные запросы

По умолчанию TanStack Query автоматически повторяет запросы при ошибках.

Стандартное значение:

retry: 3

Это означает:

  • первая попытка
  • три автоматических повтора
  • затем состояние error

Пример:

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  retry: 3
})

Отключение retry

Для некоторых ошибок повторные запросы бессмысленны.

Например:

  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found

Отключение:

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  retry: false
})

Условный retry

Параметр retry может быть функцией.

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,

  retry: (failureCount, error) => {
    if (error.status === 404) {
      return false
    }

    return failureCount < 5
  }
})

Аргументы:

  • failureCount — число неудачных попыток
  • error — объект ошибки

retryDelay — задержка между попытками

TanStack Query поддерживает настройку интервалов между повторами.

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,

  retryDelay: 1000
})

Задержка указывается в миллисекундах.


Экспоненциальная задержка

Наиболее распространённый подход — exponential backoff.

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,

  retryDelay: attempt =>
    Math.min(1000 * 2 ** attempt, 30000)
})

Пример:

Попытка Задержка
1 2 сек
2 4 сек
3 8 сек
4 16 сек

Ошибки мутаций

useMutation также поддерживает обработку ошибок.

const mutation = useMutation({
  mutationFn: async user => {
    const response = await fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(user)
    })

    if (!response.ok) {
      throw new Error('Ошибка создания')
    }

    return response.json()
  }
})

Состояния мутации

Мутация возвращает:

const {
  isPending,
  isSuccess,
  isError,
  error
} = mutation

Пример:

if (mutation.isError) {
  return <div>{mutation.error.message}</div>
}

onError у мутаций

Для мутаций очень часто используется колбэк onError.

const mutation = useMutation({
  mutationFn: createUser,

  onError: error => {
    console.error(error)
  }
})

Глобальные уведомления об ошибках

Через onError удобно показывать toast-уведомления.

const mutation = useMutation({
  mutationFn: createUser,

  onError: error => {
    toast.error(error.message)
  }
})

onSettled

Колбэк вызывается независимо от результата.

useMutation({
  mutationFn: createUser,

  onSettled: () => {
    console.log('Запрос завершён')
  }
})

Разница между onError и onSettled

onError

Срабатывает только при ошибке.

onError: error => {}

onSettled

Срабатывает всегда.

onSettled: (data, error) => {}

throwOnError

Позволяет пробрасывать ошибку выше по дереву компонентов.

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  throwOnError: true
})

Ошибка будет выброшена в React Error Boundary.


Error Boundary

TanStack Query интегрируется с механизмом React Error Boundary.

<ErrorBoundary fallback={<div>Ошибка приложения</div>}>
  <Users />
</ErrorBoundary>

При использовании:

throwOnError: true

ошибка попадёт в boundary вместо локального isError.


useQueryErrorResetBoundary

Позволяет сбрасывать состояние ошибок.

import {
  useQueryErrorResetBoundary
} from '@tanstack/react-query'

Пример:

function Page() {
  const { reset } = useQueryErrorResetBoundary()

  return (
    <ErrorBoundary
      onRe set={reset}
      fallbackRender={({ resetErrorBoundary }) => (
        <div>
          Ошибка
          <button onCl ick={resetErrorBoundary}>
            Повторить
          </button>
        </div>
      )}
    >
      <Users />
    </ErrorBoundary>
  )
}

Сетевые ошибки

Частая проблема — отсутствие интернета.

Пример:

queryFn: async () => {
  try {
    const response = await fetch('/api/users')

    return response.json()
  } catch {
    throw new Error('Проблема сети')
  }
}

Таймауты запросов

fetch не имеет встроенного timeout.

Используется AbortController.

queryFn: async () => {
  const controller = new AbortController()

  const timeout = setTimeout(() => {
    controller.abort()
  }, 5000)

  try {
    const response = await fetch('/api/users', {
      signal: controller.signal
    })

    return response.json()
  } finally {
    clearTimeout(timeout)
  }
}

Обработка отмены запроса

При отмене возникает ошибка AbortError.

queryFn: async () => {
  try {
    const response = await fetch('/api/users')

    return response.json()
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Запрос отменён')
    }

    throw error
  }
}

Логирование ошибок

Ошибки часто отправляются во внешние системы мониторинга.

Например:

  • Sentry
  • LogRocket
  • Datadog

Пример:

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,

  onError: error => {
    Sentry.captureException(error)
  }
})

Глобальная обработка ошибок QueryClient

Ошибки можно централизовать через QueryCache.

import {
  QueryClient,
  QueryCache
} from '@tanstack/react-query'

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: error => {
      console.error(error)
    }
  })
})

Глобальная обработка ошибок мутаций

Через MutationCache.

import {
  QueryClient,
  MutationCache
} from '@tanstack/react-query'

const queryClient = new QueryClient({
  mutationCache: new MutationCache({
    onError: error => {
      console.error(error)
    }
  })
})

Разделение ошибок по типам

Хорошей практикой считается деление ошибок на категории:

Тип Пример
Network Error Нет интернета
Auth Error 401
Validation Error Ошибка формы
Server Error 500
Business Error Логические ограничения

Обработка ошибок в Axios

Axios автоматически выбрасывает исключения при статусах 4xx и 5xx.

Поэтому код становится проще.

import axios from 'axios'

useQuery({
  queryKey: ['users'],

  queryFn: async () => {
    const response = await axios.get('/api/users')

    return response.data
  }
})

Доступ к response в Axios

catch (error) {
  console.log(error.response.status)
  console.log(error.response.data)
}

Обработка validation errors

Сервер может возвращать ошибки формы.

{
  "errors": {
    "email": "Некорректный email"
  }
}

Пример:

onError: error => {
  setFormErrors(error.response.data.errors)
}

UX при ошибках

Неправильная обработка ошибок ухудшает интерфейс.

Распространённые проблемы:

  • бесконечные retry
  • исчезновение старых данных
  • резкие скачки интерфейса
  • агрессивные сообщения
  • потеря формы при ошибке

Сохранение старых данных при ошибке

TanStack Query способен сохранять предыдущие данные.

const {
  data,
  error,
  isError
} = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
})

Если фоновое обновление завершится ошибкой:

  • data останется доступным
  • isError станет true

Это позволяет не разрушать интерфейс полностью.


Background Error

Частая ситуация:

  1. данные успешно загрузились
  2. пользователь работает с интерфейсом
  3. фоновый refetch завершился ошибкой

В этом случае не следует скрывать контент.

Правильный подход:

return (
  <>
    {isError && (
      <div>Не удалось обновить данные</div>
    )}

    <UsersTable data={data} />
  </>
)

Разделение fatal и non-fatal ошибок

Некоторые ошибки критичны, некоторые — нет.

Критичные:

  • первая загрузка не удалась
  • отсутствуют данные

Некритичные:

  • ошибка background refetch
  • временный timeout

Пример:

if (isError && !data) {
  return <FullPageError />
}

Обработка ошибок авторизации

401 обычно требует выхода из системы.

onError: error => {
  if (error.status === 401) {
    logout()
  }
}

Централизация API-слоя

На практике обработка ошибок обычно выносится в отдельный слой.

export async function api(url, options) {
  const response = await fetch(url, options)

  if (!response.ok) {
    throw new ApiError(
      'API Error',
      response.status
    )
  }

  return response.json()
}

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

useQuery({
  queryKey: ['users'],
  queryFn: () => api('/users')
})

Нормализация ошибок

Разные API возвращают ошибки в разных форматах.

Полезно приводить их к единой структуре.

class AppError extends Error {
  constructor({
    message,
    code,
    status
  }) {
    super(message)

    this.code = code
    this.status = status
  }
}

Практическая архитектура обработки ошибок

Крупные приложения обычно используют следующую схему:

  1. API Layer
  2. Нормализация ошибок
  3. Query Layer
  4. Global Handlers
  5. Error Boundary
  6. UI Notifications
  7. Локальные fallback-компоненты

Такой подход:

  • уменьшает дублирование
  • упрощает поддержку
  • стандартизирует поведение
  • делает интерфейс стабильнее
  • облегчает масштабирование проекта