Работа с удалёнными данными всегда связана с неопределённостью. Сервер может быть недоступен, сеть может потерять соединение, API может вернуть некорректный ответ, а пользователь — отправить неверные данные. TanStack Query предоставляет развитую систему обработки подобных ситуаций и позволяет централизованно управлять ошибками на всех уровнях приложения.
Ошибки в TanStack Query возникают в нескольких местах:
queryFnmutationFnПравильная архитектура обработки ошибок позволяет:
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
Основной индикатор наличия ошибки:
if (query.isError) {
return <div>Произошла ошибка</div>
}
Содержит исходную ошибку:
if (query.error instanceof Error) {
console.log(query.error.message)
}
TanStack Query использует несколько состояний:
query.status
Возможные значения:
'pending'
'success'
'error'
Пример:
if (query.status === 'error') {
return <ErrorScreen />
}
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')
}
На практике создаётся единый 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()
}
Связаны с ответом сервера:
400
401
403
404
500
Сервер отвечает успешно, но операция невозможна:
{
"success": false,
"message": "Недостаточно средств"
}
Обработка:
if (!data.success) {
throw new Error(data.message)
}
Особенно важны для mutations.
Пример:
{
"errors": {
"email": "Некорректный email"
}
}
Обычно подобные ошибки НЕ должны показываться как глобальная ошибка приложения.
const usersQuery = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
if (usersQuery.isError) {
return (
<div>
{usersQuery.error.message}
</div>
)
}
const {
data,
error,
isError
} = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
TanStack Query умеет обновлять данные в фоне.
Проблема:
В такой ситуации:
query.data
сохраняется, но:
query.isRefetchError
становится true.
Пример:
if (query.isRefetchError) {
showToast('Не удалось обновить данные')
}
Ошибка во время первой загрузки:
query.isLoadingError
Ошибка во время фонового обновления:
query.isRefetchError
Количество неудачных попыток:
query.failureCount
Последняя ошибка:
query.failureReason
По умолчанию TanStack Query повторяет запросы:
retry: 3
Это помогает переживать временные проблемы сети.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
retry: false
})
retry: 5
Можно повторять запрос только для определённых ошибок.
retry: (failureCount, error) => {
if (error.status === 404) {
return false
}
return failureCount < 3
}
retryDelay: 1000
Стандартная стратегия:
retryDelay: attempt =>
Math.min(1000 * 2 ** attempt, 30000)
Прогрессия:
1s
2s
4s
8s
16s
30s
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
throwOnError: true
})
Ошибка будет выброшена в React Error Boundary.
throwOnError: error => {
return error.status >= 500
}
Полезно для разделения:
Пример:
<ErrorBoundary fallback={<ErrorPage />}>
<UsersPage />
</ErrorBoundary>
TanStack Query предоставляет специальный механизм сброса ошибок.
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onRe set={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div>
Ошибка
<button onCl ick={resetErrorBoundary}>
Повторить
</button>
</div>
)}
>
<Users />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
const mutation = useMutation({
mutationFn: createUser
})
Состояния:
mutation.isError
mutation.error
const mutation = useMutation({
mutationFn: createUser,
onError: error => {
console.error(error)
}
})
Вызывается всегда:
onSettled: (data, error) => {
console.log(data)
console.log(error)
}
Пример:
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
)
}
})
Можно централизовать обработку всех query-ошибок.
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: error => {
console.error(error)
}
})
})
Глобальные mutation-ошибки:
const queryClient = new QueryClient({
mutationCache: new MutationCache({
onError: error => {
console.error(error)
}
})
})
Типичная архитектура:
onError: error => {
toast.error(error.message)
}
Проблема:
Решение — использовать глобальный обработчик.
Пример:
onError: error => {
Sentry.captureException(error)
}
Типичный сценарий:
onError: error => {
if (error.status === 401) {
logout()
}
}
Не каждая ошибка должна отображаться пользователю.
Пример:
retry: (count, error) => {
if (error.status === 404) {
return false
}
return true
}
if (error.status === 404) {
return <NotFoundPage />
}
if (error.status >= 500) {
return <ServerErrorPage />
}
Axios автоматически выбрасывает ошибки для HTTP-кодов вне диапазона
2xx.
Пример:
const fetchUsers = async () => {
const response = await axios.get('/users')
return response.data
}
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'
}
}
TanStack Query поддерживает AbortController.
Пример:
const fetchUsers = async ({ signal }) => {
const response = await fetch('/users', {
signal
})
return response.json()
}
if (error.name === 'AbortError') {
return
}
Такие ошибки обычно не отображаются пользователю.
Во время потери сети:
query.fetchStatus === 'paused'
Можно изменить поведение запросов:
networkMode: 'offlineFirst'
Варианты:
'online'
'always'
'offlineFirst'
При использовании Suspense ошибки автоматически пробрасываются в Error Boundary.
useSuspenseQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
Распространённая архитектура:
глобально:
локально:
Здесь:
Здесь:
Здесь:
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)
}
})
})
Ошибка:
if (!response.ok) {
return null
}
TanStack Query считает запрос успешным.
Правильно:
throw new Error()
Ошибка архитектуры:
toast.error(error.message)
в десятках компонентов.
Опасный пример:
retry: true
Может привести к чрезмерной нагрузке.
Фоновое обновление может регулярно падать, оставаясь незаметным.
Позволяет стандартизировать:
Упрощает:
Критические:
Некритические:
Остальные ошибки лучше обрабатывать локально.
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)
}
})
})