В приложениях с большим количеством запросов локальная обработка
ошибок быстро превращается в источник дублирования. Повторяющиеся
try/catch, одинаковые уведомления, логирование и проверки
статусов начинают расползаться по компонентам, хукам и сервисам.
TanStack Query предоставляет механизм глобальных обработчиков ошибок
через конфигурацию QueryClient. Такие обработчики позволяют
централизованно реагировать на ошибки:
queries);mutations);Глобальные обработчики особенно полезны для:
401, 403,
500;В TanStack Query существует несколько уровней обработки ошибок:
Локальный уровень:
onError внутри useQuery;onError внутри useMutation.Глобальный уровень:
QueryCache;MutationCache.Error Boundaries:
useErrorBoundary.Низкоуровневые interceptors:
Глобальная обработка в TanStack Query располагается между сетевым слоем и UI-компонентами.
import {
QueryClient,
QueryCache
} from '@tanstack/react-query'
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
console.error('Ошибка запроса:', error)
console.log('Query key:', query.queryKey)
}
})
})
Обработчик получает:
(error, query)
Где:
error — объект ошибки;query — экземпляр Query.Одна из самых полезных возможностей — анализ ключа запроса.
onError: (error, query) => {
console.log(query.queryKey)
}
Пример:
['users', 15]
или:
['posts', {
page: 2
}]
Это позволяет:
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
const [scope] = query.queryKey
if (scope === 'auth') {
console.error('Ошибка авторизации')
}
if (scope === 'payments') {
console.error('Ошибка платежного API')
}
}
})
})
Мутации имеют собственный кэш.
import {
QueryClient,
MutationCache
} from '@tanstack/react-query'
const queryClient = new QueryClient({
mutationCache: new MutationCache({
onError: (error, variables, context, mutation) => {
console.error(error)
}
})
})
Сигнатура:
(error, variables, context, mutation)
const queryClient = new QueryClient({
mutationCache: new MutationCache({
onError: (error, variables, context, mutation) => {
console.log(mutation.options.mutationKey)
}
})
})
Пример:
['update-user']
или:
['delete-post']
onError: (error, variables, context, mutation) => {
const key = mutation.options.mutationKey?.[0]
switch (key) {
case 'create-order':
console.error('Ошибка создания заказа')
break
case 'upload-avatar':
console.error('Ошибка загрузки файла')
break
}
}
Наиболее популярный сценарий — глобальный показ уведомлений.
import { toast } from 'react-toastify'
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error) => {
toast.error('Не удалось загрузить данные')
}
})
})
Иногда уведомления от background refetch раздражают пользователей.
Например:
Проверка состояния запроса:
onError: (error, query) => {
if (query.state.data !== undefined) {
return
}
toast.error('Ошибка загрузки')
}
Логика:
Чаще всего приложения используют Axios.
onError: (error) => {
if (error.response?.status === 401) {
console.log('Не авторизован')
}
}
onError: (error) => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
}
onError: (error) => {
if (error.response?.status === 403) {
console.error('Недостаточно прав')
}
}
onError: (error) => {
if (error.response?.status >= 500) {
console.error('Ошибка сервера')
}
}
Важно понимать разницу:
Отвечают за:
Отвечает за:
import axios from 'axios'
export const api = axios.create({
baseURL: '/api'
})
api.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
console.error('Refresh token flow')
}
return Promise.reject(error)
}
)
TanStack Query:
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error) => {
console.error('UI-level error')
}
})
})
import * as Sentry from '@sentry/react'
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
Sentry.captureException(error, {
extra: {
queryKey: query.queryKey
}
})
}
})
})
mutationCache: new MutationCache({
onError: (
error,
variables,
context,
mutation
) => {
Sentry.captureException(error, {
extra: {
variables,
mutationKey: mutation.options.mutationKey
}
})
}
})
TanStack Query умеет пробрасывать ошибки в React Error Boundary.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
useErrorBoundary: true
})
Теперь ошибка не останется внутри query-state, а попадёт в boundary.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
useErrorBoundary: (error) => {
return error.response?.status >= 500
}
})
Сценарий:
400 и 404 обрабатываются локально;500 вызывает глобальную аварийную страницу.Одна из главных особенностей TanStack Query — разделение:
const {
data,
error,
isError
} = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
Если:
data !== undefined
значит:
onError: (error, query) => {
const hasCachedData =
query.state.data !== undefined
if (!hasCachedData) {
toast.error('Не удалось загрузить данные')
return
}
console.warn('Ошибка фонового обновления')
}
По умолчанию TanStack Query делает retry.
retry: 3
Это означает:
onError вызывается только после исчерпания retry.retry: (failureCount, error) => {
console.log(failureCount)
return failureCount < 3
}
retry: (failureCount, error) => {
if (error.response?.status === 401) {
return false
}
return failureCount < 3
}
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: (error) => {
console.error(error)
}
},
mutations: {
onError: (error) => {
console.error(error)
}
}
}
})
Работает как дефолт для каждого запроса.
Можно переопределить локально:
useQuery({
onError: () => {}
})
Всегда вызывается глобально.
Не переопределяется.
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: globalErrorHandler
}),
defaultOptions: {
queries: {
onError: localDefaultHandler
}
}
})
Порядок вызовов:
onError;onError;onError.Без контроля можно получить:
В результате пользователь видит несколько одинаковых уведомлений.
Наиболее стабильная архитектура:
Иногда ошибку помечают вручную:
const error = new Error('API Error')
error.isHandled = true
Проверка:
onError: (error) => {
if (error.isHandled) {
return
}
toast.error(error.message)
}
class ApiError extends Error {
constructor(
message,
status
) {
super(message)
this.status = status
}
}
async function fetchUsers() {
const response = await fetch('/users')
if (!response.ok) {
throw new ApiError(
'Ошибка API',
response.status
)
}
return response.json()
}
onError: (error) => {
if (error instanceof ApiError) {
console.log(error.status)
}
}
Не все ошибки должны отображаться.
Например:
404;onError: (error) => {
if (error.name === 'AbortError') {
return
}
toast.error(error.message)
}
onError: (error) => {
if (error.response?.status === 404) {
return
}
toast.error('Ошибка')
}
На сервере:
window;localStorage;onError: (error) => {
if (typeof window === 'undefined') {
return
}
toast.error('Ошибка')
}
Крупные приложения часто выносят обработку ошибок в отдельный сервис.
export function handleApiError(error) {
if (error.response?.status === 401) {
logout()
return
}
if (error.response?.status >= 500) {
showServerError()
return
}
showGenericError()
}
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: handleApiError
}),
mutationCache: new MutationCache({
onError: handleApiError
})
})
import {
QueryClient,
QueryCache,
MutationCache
} from '@tanstack/react-query'
import { toast } from 'react-toastify'
function globalErrorHandler(error, query) {
if (error.name === 'AbortError') {
return
}
const status = error.response?.status
if (status === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
return
}
if (status === 403) {
toast.error('Недостаточно прав')
return
}
if (status >= 500) {
toast.error('Ошибка сервера')
return
}
const hasCachedData =
query?.state?.data !== undefined
if (!hasCachedData) {
toast.error(
error.message ||
'Ошибка запроса'
)
}
}
export const queryClient =
new QueryClient({
queryCache: new QueryCache({
onError: globalErrorHandler
}),
mutationCache: new MutationCache({
onError: globalErrorHandler
})
})
Плохой подход:
async function fetchUsers() {
try {
const response = await api.get('/users')
return response.data
} catch (error) {
toast.error('Ошибка')
throw error
}
}
Проблемы:
Неправильно:
toast.error('Ошибка')
Для любых случаев:
401;403;404;500;Грамотная архитектура требует категоризации ошибок.
Без проверки кэшированных данных приложение может постоянно показывать уведомления при нестабильной сети.
Если Axios interceptor уже выполнил logout, TanStack Query не должен делать это повторно.