Работа с удалёнными данными всегда связана с потенциальными сбоями. Сервер может быть недоступен, сеть — нестабильной, API — вернуть некорректный ответ, а пользователь — потерять интернет-соединение во время выполнения запроса. TanStack Query предоставляет развитую систему обработки ошибок, позволяющую централизованно управлять состояниями сбоев, повторными попытками, уведомлениями и поведением интерфейса.
Внутри TanStack Query ошибка считается полноценным состоянием запроса наряду с загрузкой и успешным завершением. Любой запрос может находиться в одном из состояний:
При возникновении исключения внутри 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 не анализирует 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 возвращает несколько свойств, связанных с
ошибками.
Содержит объект ошибки.
const { error } = useQuery(...)
Обычно это экземпляр Error.
console.log(error.message)
Флаг, указывающий, что запрос завершился ошибкой.
const { isError } = useQuery(...)
Текстовое состояние запроса.
const { status } = useQuery(...)
Возможные значения:
pending
success
error
Отражает физическое состояние сетевого запроса.
const { fetchStatus } = useQuery(...)
Возможные значения:
fetching
paused
idle
Иногда запрос имеет состояние error, но одновременно
может повторно выполняться в фоне.
Внутри 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>
}
По умолчанию TanStack Query автоматически повторяет запросы при ошибках.
Стандартное значение:
retry: 3
Это означает:
errorПример:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
retry: 3
})
Для некоторых ошибок повторные запросы бессмысленны.
Например:
Отключение:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
retry: false
})
Параметр retry может быть функцией.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
retry: (failureCount, error) => {
if (error.status === 404) {
return false
}
return failureCount < 5
}
})
Аргументы:
failureCount — число неудачных попытокerror — объект ошибки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.
const mutation = useMutation({
mutationFn: createUser,
onError: error => {
console.error(error)
}
})
Через onError удобно показывать toast-уведомления.
const mutation = useMutation({
mutationFn: createUser,
onError: error => {
toast.error(error.message)
}
})
Колбэк вызывается независимо от результата.
useMutation({
mutationFn: createUser,
onSettled: () => {
console.log('Запрос завершён')
}
})
Срабатывает только при ошибке.
onError: error => {}
Срабатывает всегда.
onSettled: (data, error) => {}
Позволяет пробрасывать ошибку выше по дереву компонентов.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
throwOnError: true
})
Ошибка будет выброшена в React Error Boundary.
TanStack Query интегрируется с механизмом React Error Boundary.
<ErrorBoundary fallback={<div>Ошибка приложения</div>}>
<Users />
</ErrorBoundary>
При использовании:
throwOnError: true
ошибка попадёт в boundary вместо локального isError.
Позволяет сбрасывать состояние ошибок.
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
}
}
Ошибки часто отправляются во внешние системы мониторинга.
Например:
Пример:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
onError: error => {
Sentry.captureException(error)
}
})
Ошибки можно централизовать через 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 автоматически выбрасывает исключения при статусах
4xx и 5xx.
Поэтому код становится проще.
import axios from 'axios'
useQuery({
queryKey: ['users'],
queryFn: async () => {
const response = await axios.get('/api/users')
return response.data
}
})
catch (error) {
console.log(error.response.status)
console.log(error.response.data)
}
Сервер может возвращать ошибки формы.
{
"errors": {
"email": "Некорректный email"
}
}
Пример:
onError: error => {
setFormErrors(error.response.data.errors)
}
Неправильная обработка ошибок ухудшает интерфейс.
Распространённые проблемы:
TanStack Query способен сохранять предыдущие данные.
const {
data,
error,
isError
} = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
Если фоновое обновление завершится ошибкой:
data останется доступнымisError станет trueЭто позволяет не разрушать интерфейс полностью.
Частая ситуация:
В этом случае не следует скрывать контент.
Правильный подход:
return (
<>
{isError && (
<div>Не удалось обновить данные</div>
)}
<UsersTable data={data} />
</>
)
Некоторые ошибки критичны, некоторые — нет.
Критичные:
Некритичные:
Пример:
if (isError && !data) {
return <FullPageError />
}
401 обычно требует выхода из системы.
onError: error => {
if (error.status === 401) {
logout()
}
}
На практике обработка ошибок обычно выносится в отдельный слой.
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
}
}
Крупные приложения обычно используют следующую схему:
Такой подход: