В приложениях с большим количеством запросов локальная обработка
ошибок внутри каждого useQuery и useMutation
быстро приводит к дублированию кода, несогласованным уведомлениям и
сложностям поддержки. TanStack Query предоставляет механизмы
централизованной обработки ошибок на уровне:
QueryClientQueryCacheMutationCacheГлобальная обработка ошибок позволяет:
Ошибки могут появляться в нескольких местах:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
Если fetchUsers выбрасывает исключение или возвращает
rejected Promise, TanStack Query переводит запрос в состояние
ошибки.
useMutation({
mutationFn: createUser
})
Ошибки mutation-функций работают аналогично query.
Например:
{
"error": "EMAIL_ALREADY_EXISTS"
}
Сервер может вернуть HTTP 200, но содержать логическую ошибку.
Наиболее распространённые:
Стандартный вариант:
const query = useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
onError(error) {
console.error(error)
}
})
Недостатки:
Основной механизм глобальной обработки query-ошибок —
QueryCache.
import {
QueryClient,
QueryCache
} from '@tanstack/react-query'
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError(error, query) {
console.error('Global query error:', error)
}
})
})
Теперь любая ошибка query будет проходить через единый обработчик.
Объект ошибки:
onError(error)
Может содержать:
Экземпляр query:
onError(error, query)
Позволяет получить:
query.queryKey
query.state
query.meta
Часто требуется разное поведение для разных запросов.
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError(error, query) {
if (query.queryKey[0] === 'profile') {
console.error('Ошибка профиля')
}
if (query.queryKey[0] === 'admin') {
console.error('Ошибка административного API')
}
}
})
})
Для мутаций используется MutationCache.
import {
QueryClient,
MutationCache
} from '@tanstack/react-query'
const queryClient = new QueryClient({
mutationCache: new MutationCache({
onError(error, variables, context, mutation) {
console.error('Mutation error:', error)
}
})
})
Ошибка mutation.
Аргументы мутации:
mutationFn(variables)
Пример:
{
email: 'test@test.com'
}
Контекст optimistic updates.
Экземпляр mutation:
mutation.options
mutation.state
mutation.meta
Одна из главных задач глобальной обработки — единая система уведомлений.
Пример с toast-системой:
queryCache: new QueryCache({
onError(error) {
toast.error('Произошла ошибка')
}
})
Без защиты несколько компонентов могут показать одинаковые ошибки одновременно.
Проблемный сценарий:
useQuery(...)
useQuery(...)
useQuery(...)
Если сервер недоступен:
Распространённый подход:
const shownErrors = new Set()
function notifyOnce(message: string) {
if (shownErrors.has(message)) {
return
}
shownErrors.add(message)
toast.error(message)
setTimeout(() => {
shownErrors.delete(message)
}, 3000)
}
Использование:
queryCache: new QueryCache({
onError(error) {
notifyOnce('Сервер недоступен')
}
})
Обычно обработка строится вокруг кодов ответа.
Пример:
onError(error) {
const status = error.response?.status
switch (status) {
case 401:
break
case 403:
break
case 404:
break
case 500:
break
}
}
Наиболее важный сценарий — истечение авторизации.
queryCache: new QueryCache({
onError(error) {
if (error.response?.status === 401) {
logout()
}
}
})
Часто требуется очистка приложения:
function logout() {
localStorage.removeItem('token')
window.location.href = '/login'
}
После выхода необходимо очищать cache.
function logout() {
queryClient.clear()
localStorage.removeItem('token')
window.location.href = '/login'
}
Более сложная архитектура:
TanStack Query обычно не занимается этим напрямую — логика располагается в HTTP-клиенте.
Наиболее распространённый вариант — interceptor.
import axios from 'axios'
const api = axios.create({
baseURL: '/api'
})
api.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401) {
await refreshToken()
}
return Promise.reject(error)
}
)
Query-функция становится максимально простой:
async function fetchUsers() {
const response = await api.get('/users')
return response.data
}
Все:
могут выполняться централизованно.
Разные API возвращают ошибки в разных форматах.
Пример проблем:
{
"message": "Invalid token"
}
{
"error": "INVALID_TOKEN"
}
{
"errors": [
{
"message": "Validation failed"
}
]
}
Полезно создавать универсальную структуру.
type AppError = {
status: number
code: string
message: string
}
function normalizeError(error): AppError {
return {
status: error.response?.status ?? 500,
code: error.response?.data?.code ?? 'UNKNOWN',
message:
error.response?.data?.message ??
'Unexpected error'
}
}
onError(error) {
const appError = normalizeError(error)
toast.error(appError.message)
}
TanStack Query поддерживает поле meta.
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
meta: {
silent: true
}
})
queryCache: new QueryCache({
onError(error, query) {
if (query.meta?.silent) {
return
}
toast.error('Ошибка запроса')
}
})
Полезный паттерн для:
Не каждая ошибка должна показываться пользователю.
Пример:
if (status >= 500) {
toast.error('Ошибка сервера')
}
Но:
404
может быть частью обычной логики приложения.
Частая проблема — уведомления при скрытом refetch.
Например:
refetchInterval: 5000
Если сервер недоступен:
onError(error, query) {
if (query.state.data !== undefined) {
return
}
toast.error('Ошибка загрузки')
}
Если данные уже были загружены ранее, ошибка refetch может не отображаться.
По умолчанию TanStack Query повторяет запросы.
retry: 3
Важно понимать:
retry(failureCount, error) {
if (error.response?.status === 404) {
return false
}
return failureCount < 3
}
retry(failureCount, error) {
if (error.response?.status === 401) {
return false
}
return true
}
Централизованные настройки:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 2
},
mutations: {
retry: false
}
}
})
TanStack Query умеет пробрасывать ошибки в React Error Boundary.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
useErrorBoundary: true
})
Вместо локального состояния ошибки:
isError
error
ошибка будет выброшена вверх по дереву React.
Пример:
<ErrorBoundary fallback={<ErrorScreen />}>
<UsersPage />
</ErrorBoundary>
Подходит для:
Не подходит для:
Можно использовать функцию:
useErrorBoundary(error) {
return error.response?.status >= 500
}
Пример архитектуры:
| Тип ошибки | Поведение |
|---|---|
| 400 | показать validation |
| 401 | logout |
| 403 | redirect |
| 404 | показать пустое состояние |
| 500 | Error Boundary |
Часто ошибки мутаций относятся к validation.
Пример:
{
"errors": {
"email": "Already exists"
}
}
Такие ошибки обычно не должны идти в глобальный toast.
Через meta:
useMutation({
mutationFn: createUser,
meta: {
skipGlobalError: true
}
})
mutationCache: new MutationCache({
onError(error, variables, context, mutation) {
if (mutation.meta?.skipGlobalError) {
return
}
toast.error('Ошибка операции')
}
})
Глобальная обработка — идеальное место для интеграции:
queryCache: new QueryCache({
onError(error, query) {
Sentry.captureException(error, {
extra: {
queryKey: query.queryKey
}
})
}
})
Не все ошибки нужно отправлять в мониторинг.
Пример:
if (status >= 500) {
Sentry.captureException(error)
}
Иногда ошибка означает отсутствие интернета.
if (!navigator.onLine) {
toast.error('Нет соединения с интернетом')
}
TanStack Query поддерживает сетевые режимы.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
networkMode: 'offlineFirst'
})
Prefetch-запросы тоже могут вызывать глобальные ошибки.
queryClient.prefetchQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
Часто для них используется:
meta: {
silent: true
}
В больших проектах создают отдельный сервис.
class ErrorService {
handle(error) {
const normalized = normalizeError(error)
this.log(normalized)
this.notify(normalized)
}
log(error) {}
notify(error) {}
}
const errorService = new ErrorService()
queryCache: new QueryCache({
onError(error) {
errorService.handle(error)
}
})
В TypeScript полезно создавать собственные классы.
class ApiError extends Error {
status: number
code: string
constructor(message, status, code) {
super(message)
this.status = status
this.code = code
}
}
if (error instanceof ApiError) {
console.log(error.status)
}
Типичная схема:
HTTP-клиент:
TanStack Query:
UI:
Monitoring:
Одинаковые уведомления из:
onError() {}
в десятках компонентов приводит к хаосу.
Validation — часть UI-логики формы, а не глобальная авария.
Приводит к бесконечным циклам refresh-token.
Постоянные уведомления ухудшают UX.
Разные форматы ошибок усложняют поддержку и типизацию.
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError(error, query) {
if (query.meta?.silent) {
return
}
const normalized = normalizeError(error)
if (normalized.status >= 500) {
toast.error('Ошибка сервера')
}
if (normalized.status === 401) {
logout()
}
}
}),
mutationCache: new MutationCache({
onError(error, variables, context, mutation) {
if (mutation.meta?.skipGlobalError) {
return
}
const normalized = normalizeError(error)
toast.error(normalized.message)
}
}),
defaultOptions: {
queries: {
retry(failureCount, error) {
if (error.response?.status === 401) {
return false
}
return failureCount < 3
}
}
}
})