Типы ошибок и их обработка

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

Ошибки в TanStack Query возникают в нескольких местах:

  • при выполнении queryFn
  • при выполнении mutationFn
  • во время повторных запросов
  • при трансформации данных
  • внутри глобальных обработчиков
  • во время фоновых обновлений
  • при отмене запросов
  • при работе с offline-режимом

Правильная архитектура обработки ошибок позволяет:

  • избежать дублирования кода
  • централизовать уведомления
  • разделить критические и некритические ошибки
  • реализовать fallback UI
  • автоматически повторять временные сбои
  • корректно логировать проблемы
  • отделять бизнес-ошибки от сетевых

Базовая структура ошибок

TanStack Query не создаёт собственный тип ошибок. Любая ошибка формируется внутри queryFn или mutationFn.

Простейший пример:

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

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

  return response.json()
}

Ошибка автоматически попадёт в состояние запроса:

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

Теперь доступны:

query.error
query.isError
query.status

Состояния ошибки

Флаг isError

Основной индикатор наличия ошибки:

if (query.isError) {
  return <div>Произошла ошибка</div>
}

Объект error

Содержит исходную ошибку:

if (query.error instanceof Error) {
  console.log(query.error.message)
}

Статус запроса

TanStack Query использует несколько состояний:

query.status

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

'pending'
'success'
'error'

Пример:

if (query.status === 'error') {
  return <ErrorScreen />
}

Ошибки HTTP-запросов

Особенность fetch

API fetch не выбрасывает исключения при HTTP-ошибках.

Этот код НЕ вызовет ошибку:

await fetch('/api/users')

Даже если сервер вернул:

500 Internal Server Error

Поэтому необходима ручная проверка:

const response = await fetch('/api/users')

if (!response.ok) {
  throw new Error('Server error')
}

Универсальная HTTP-обёртка

На практике создаётся единый API-клиент:

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

  if (!response.ok) {
    const errorBody = await response.json().catch(() => null)

    throw {
      status: response.status,
      message: errorBody?.message || 'Unknown error',
      body: errorBody
    }
  }

  return response.json()
}

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

const fetchUsers = () => request('/api/users')

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

Кастомный класс

Сложные приложения обычно используют собственные классы ошибок.

Пример:

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

    this.name = 'ApiError'
    this.status = status
    this.payload = payload
  }
}

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

if (!response.ok) {
  throw new ApiError(
    'Ошибка авторизации',
    response.status,
    await response.json()
  )
}

Проверка:

if (query.error instanceof ApiError) {
  console.log(query.error.status)
}

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

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

Возникают при отсутствии соединения:

TypeError: Failed to fetch

Пример обработки:

if (error instanceof TypeError) {
  showOfflineNotification()
}

HTTP-ошибки

Связаны с ответом сервера:

400
401
403
404
500

Бизнес-ошибки

Сервер отвечает успешно, но операция невозможна:

{
  "success": false,
  "message": "Недостаточно средств"
}

Обработка:

if (!data.success) {
  throw new Error(data.message)
}

Ошибки валидации

Особенно важны для mutations.

Пример:

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

Обычно подобные ошибки НЕ должны показываться как глобальная ошибка приложения.


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

Локальная обработка

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

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

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

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

Ошибки фонового refetch

TanStack Query умеет обновлять данные в фоне.

Проблема:

  • первоначальная загрузка успешна
  • subsequent refetch завершается ошибкой
  • данные уже есть в кэше

В такой ситуации:

query.data

сохраняется, но:

query.isRefetchError

становится true.

Пример:

if (query.isRefetchError) {
  showToast('Не удалось обновить данные')
}

Флаги ошибок

isLoadingError

Ошибка во время первой загрузки:

query.isLoadingError

isRefetchError

Ошибка во время фонового обновления:

query.isRefetchError

failureCount

Количество неудачных попыток:

query.failureCount

failureReason

Последняя ошибка:

query.failureReason

Retry-механизм

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

По умолчанию TanStack Query повторяет запросы:

retry: 3

Это помогает переживать временные проблемы сети.


Отключение retry

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

Настройка количества попыток

retry: 5

Условный retry

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

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

  return failureCount < 3
}

Retry Delay

Интервал между попытками

retryDelay: 1000

Exponential Backoff

Стандартная стратегия:

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

Прогрессия:

1s
2s
4s
8s
16s
30s

throwOnError

Проброс ошибок в Error Boundary

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

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


Условный throwOnError

throwOnError: error => {
  return error.status >= 500
}

Полезно для разделения:

  • критических ошибок
  • локальных ошибок интерфейса

Error Boundaries

Интеграция с React

Пример:

<ErrorBoundary fallback={<ErrorPage />}>
  <UsersPage />
</ErrorBoundary>

QueryErrorResetBoundary

TanStack Query предоставляет специальный механизм сброса ошибок.

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

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

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

const mutation = useMutation({
  mutationFn: createUser
})

Состояния:

mutation.isError
mutation.error

onError

Локальный обработчик

const mutation = useMutation({
  mutationFn: createUser,

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

onSettled

Вызывается всегда:

onSettled: (data, error) => {
  console.log(data)
  console.log(error)
}

Ошибки optimistic updates

Rollback при ошибке

Пример:

useMutation({
  mutationFn: updateTodo,

  onMutate: async updatedTodo => {
    await queryClient.cancelQueries({
      queryKey: ['todos']
    })

    const previousTodos =
      queryClient.getQueryData(['todos'])

    queryClient.setQueryData(
      ['todos'],
      old => {
        return old.map(todo =>
          todo.id === updatedTodo.id
            ? updatedTodo
            : todo
        )
      }
    )

    return { previousTodos }
  },

  onError: (error, variables, context) => {
    queryClient.setQueryData(
      ['todos'],
      context.previousTodos
    )
  }
})

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

QueryCache

Можно централизовать обработку всех query-ошибок.

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

MutationCache

Глобальные mutation-ошибки:

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

Централизованные уведомления

Типичная архитектура:

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

Исключение дубликатов уведомлений

Проблема:

  • несколько компонентов используют один query
  • ошибка возникает одновременно
  • пользователь получает несколько toast-сообщений

Решение — использовать глобальный обработчик.


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

Интеграция с monitoring-системами

Пример:

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

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

Перехват 401

Типичный сценарий:

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

Игнорирование некоторых ошибок

Не каждая ошибка должна отображаться пользователю.

Пример:

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

  return true
}

Обработка 404

Отдельный UI

if (error.status === 404) {
  return <NotFoundPage />
}

Обработка 500

Серверные ошибки

if (error.status >= 500) {
  return <ServerErrorPage />
}

Работа с Axios

Axios автоматически выбрасывает ошибки для HTTP-кодов вне диапазона 2xx.

Пример:

const fetchUsers = async () => {
  const response = await axios.get('/users')

  return response.data
}

Структура AxiosError

error.response
error.response.status
error.response.data

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

Часто приложение использует единый формат ошибок независимо от HTTP-клиента.

Пример:

export function normalizeError(error) {
  if (axios.isAxiosError(error)) {
    return {
      message: error.message,
      status: error.response?.status
    }
  }

  return {
    message: 'Unknown error'
  }
}

AbortError

Отмена запросов

TanStack Query поддерживает AbortController.

Пример:

const fetchUsers = async ({ signal }) => {
  const response = await fetch('/users', {
    signal
  })

  return response.json()
}

Игнорирование AbortError

if (error.name === 'AbortError') {
  return
}

Такие ошибки обычно не отображаются пользователю.


Offline-режим

Ошибки соединения

Во время потери сети:

query.fetchStatus === 'paused'

networkMode

Можно изменить поведение запросов:

networkMode: 'offlineFirst'

Варианты:

'online'
'always'
'offlineFirst'

Suspense и ошибки

При использовании Suspense ошибки автоматически пробрасываются в Error Boundary.

useSuspenseQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
})

Комбинирование локальной и глобальной обработки

Распространённая архитектура:

  • глобально:

    • логирование
    • toast
    • мониторинг
    • auth redirect
  • локально:

    • UI-компоненты
    • формы
    • fallback интерфейсы

Архитектура production-приложений

Уровень API-клиента

Здесь:

  • нормализуются ошибки
  • создаются классы ошибок
  • анализируются HTTP-коды

Уровень QueryClient

Здесь:

  • глобальные уведомления
  • retry
  • логирование
  • auth handling

Уровень компонентов

Здесь:

  • локальный UI
  • fallback
  • формы
  • специальные сценарии

Типичная production-схема

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry(failureCount, error) {
        if (error.status === 404) {
          return false
        }

        return failureCount < 3
      },

      throwOnError(error) {
        return error.status >= 500
      }
    }
  },

  queryCache: new QueryCache({
    onError(error) {
      toast.error(error.message)
    }
  })
})

Антипаттерны

Отсутствие throw

Ошибка:

if (!response.ok) {
  return null
}

TanStack Query считает запрос успешным.

Правильно:

throw new Error()

Обработка ошибок в каждом компоненте

Ошибка архитектуры:

toast.error(error.message)

в десятках компонентов.


Бесконечные retry

Опасный пример:

retry: true

Может привести к чрезмерной нагрузке.


Игнорирование refetch-ошибок

Фоновое обновление может регулярно падать, оставаясь незаметным.


Рекомендации для крупных приложений

Использование собственного ApiError

Позволяет стандартизировать:

  • статус
  • код ошибки
  • payload
  • message

Централизованный request-клиент

Упрощает:

  • авторизацию
  • refresh token
  • логирование
  • retry
  • telemetry

Разделение ошибок по критичности

Критические:

  • 500
  • auth errors
  • corruption

Некритические:

  • временная потеря сети
  • refetch failure
  • validation errors

Error Boundary только для критических ошибок

Остальные ошибки лучше обрабатывать локально.


Пример комплексной обработки

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

    this.status = status
  }
}

async function request(url) {
  const response = await fetch(url)

  if (!response.ok) {
    throw new ApiError(
      'Request failed',
      response.status
    )
  }

  return response.json()
}

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: (count, error) => {
        if (error.status === 404) {
          return false
        }

        return count < 3
      },

      throwOnError: error => {
        return error.status >= 500
      }
    }
  },

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