Логирование ошибок — важная часть эксплуатации приложений, использующих TanStack Query. Библиотека активно управляет сетевыми запросами, кэшированием, повторными попытками, фоновыми обновлениями и синхронизацией состояния. При большом количестве асинхронных операций ошибки начинают появляться одновременно в разных слоях приложения:
Без централизованного логирования диагностика становится сложной. Ошибки теряются между компонентами, дублируются в консоли, попадают в UI несколько раз или вообще не фиксируются.
Ошибки могут возникать на нескольких уровнях.
Наиболее распространённый источник — исключения внутри
queryFn.
const fetchUsers = async () => {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Ошибка загрузки пользователей');
}
return response.json();
};
Если queryFn выбрасывает исключение, TanStack Query:
error;Mutations создают отдельный поток ошибок.
const createUser = async (payload) => {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error('Ошибка создания пользователя');
}
return response.json();
};
Особенность mutations заключается в том, что ошибки часто связаны:
Refetch может происходить:
Ошибка в background refetch особенно опасна, потому что пользователь может продолжать видеть старые данные, не подозревая о проблеме.
TanStack Query не навязывает формат ошибки.
В error может находиться:
Error;Пример:
const {
data,
error,
isError
} = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
Для логирования желательно приводить ошибки к единому формату.
Разные источники возвращают разные структуры:
throw new Error('Network error');
throw axiosError;
throw {
code: 'VALIDATION_ERROR',
fields: ['email']
};
export const normalizeError = (error) => {
if (error instanceof Error) {
return {
message: error.message,
stack: error.stack,
type: error.name
};
}
if (typeof error === 'object' && error !== null) {
return {
message: error.message || 'Unknown error',
type: error.code || 'CUSTOM_ERROR',
raw: error
};
}
return {
message: String(error),
type: 'UNKNOWN'
};
};
Такой подход упрощает:
Каждый query может иметь собственный обработчик.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
onError: (error) => {
console.error('Ошибка users query', error);
}
});
Аналогично для mutations:
useMutation({
mutationFn: createUser,
onError: (error) => {
console.error('Ошибка mutation', error);
}
});
Если в приложении сотни queries, локальные onError
быстро приводят к проблемам:
TanStack Query позволяет отслеживать ошибки глобально через
QueryCache.
import {
QueryClient,
QueryCache
} from '@tanstack/react-query';
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
console.error('Global query error');
console.error({
queryKey: query.queryKey,
error
});
}
})
});
Преимущества:
Для mutations используется отдельный cache.
import {
MutationCache,
QueryClient
} from '@tanstack/react-query';
const queryClient = new QueryClient({
mutationCache: new MutationCache({
onError: (error, variables, context, mutation) => {
console.error('Mutation error');
console.error({
mutationKey: mutation.options.mutationKey,
variables,
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: {
mutationKey: mutation.options.mutationKey,
variables
}
});
}
})
Контекст mutation особенно полезен при расследовании:
TanStack Query поддерживает замену стандартного logger.
import { setLogger } from '@tanstack/react-query';
setLogger({
log: (...args) => {
console.log(...args);
},
warn: (...args) => {
console.warn(...args);
},
error: (...args) => {
console.error(...args);
}
});
В production окружении консольные ошибки часто отключаются.
setLogger({
log: () => {},
warn: () => {},
error: (error) => {
sendToMonitoring(error);
}
});
Простой вывод в консоль плохо масштабируется.
console.error(error);
Проблемы:
const logQueryError = ({
error,
query
}) => {
const normalized = normalizeError(error);
logger.error({
type: 'QUERY_ERROR',
queryKey: query.queryKey,
message: normalized.message,
stack: normalized.stack,
timestamp: Date.now()
});
};
По умолчанию TanStack Query делает retry failed requests.
Если логировать каждую попытку:
retry: 3
то одна ошибка может попасть в лог четыре раза:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
retry: 3,
onError: (error) => {
console.error('Final query error', error);
}
});
onError вызывается после завершения retry chain.
Иногда необходимо отслеживать все попытки.
retry: (failureCount, error) => {
logger.warn({
type: 'RETRY_ATTEMPT',
failureCount,
error
});
return failureCount < 3;
}
if (!navigator.onLine) {
logger.warn({
type: 'OFFLINE_ERROR'
});
}
if (response.status >= 500) {
logger.error({
type: 'SERVER_ERROR',
status: response.status
});
}
if (response.status === 422) {
logger.warn({
type: 'VALIDATION_ERROR'
});
}
if (response.status === 401) {
logger.warn({
type: 'AUTH_ERROR'
});
}
Query key — основной идентификатор запроса.
onError: (error, query) => {
logger.error({
queryKey: query.queryKey
});
}
onError: (error, query) => {
logger.error({
state: query.state
});
}
Поле query.state содержит:
onError: (error, variables) => {
logger.error({
variables
});
}
Нельзя логировать:
Опасный пример:
logger.error({
headers: request.headers
});
const sanitize = (payload) => {
return {
...payload,
password: undefined,
token: undefined
};
};
useMutation({
mutationFn: updateUser,
onMutate: async (newUser) => {
const previous =
queryClient.getQueryData(['user']);
queryClient.setQueryData(
['user'],
newUser
);
return { previous };
},
onError: (error, variables, context) => {
logger.error({
type: 'OPTIMISTIC_UPDATE_FAILED',
error
});
queryClient.setQueryData(
['user'],
context.previous
);
}
});
Background refetch может ломаться незаметно.
useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
refetchInterval: 10000,
onError: (error) => {
logger.warn({
type: 'BACKGROUND_REFETCH_ERROR',
error
});
}
});
TanStack Query умеет работать в offline-first режимах.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
networkMode: 'offlineFirst'
});
window.addEventListener('offline', () => {
logger.warn({
type: 'NETWORK_OFFLINE'
});
});
Для сложных приложений полезно связывать запросы между собой.
const correlationId = crypto.randomUUID();
const fetchUsers = async () => {
const correlationId = crypto.randomUUID();
logger.info({
correlationId,
type: 'REQUEST_START'
});
const response = await fetch('/api/users', {
headers: {
'X-Correlation-ID': correlationId
}
});
return response.json();
};
import axios from 'axios';
const api = axios.create();
api.interceptors.response.use(
response => response,
error => {
logger.error({
type: 'HTTP_ERROR',
status: error.response?.status
});
return Promise.reject(error);
}
);
const fetchUsers = async () => {
const startedAt = performance.now();
try {
const response = await fetch('/api/users');
return response.json();
} finally {
logger.info({
duration:
performance.now() - startedAt
});
}
};
Devtools помогают анализировать:
Пример подключения:
import {
ReactQueryDevtools
} from '@tanstack/react-query-devtools';
<ReactQueryDevtools initialIsOpen={false} />
Крупные приложения обычно строят отдельный слой обработки ошибок:
Query/Mutation
↓
normalizeError()
↓
logger
↓
monitoring service
↓
analytics
export class ErrorService {
static capture(error, metadata = {}) {
const normalized =
normalizeError(error);
logger.error({
...normalized,
...metadata,
timestamp: Date.now()
});
}
}
onError: (error, query) => {
ErrorService.capture(error, {
queryKey: query.queryKey,
type: 'QUERY_ERROR'
});
}
Во время разработки полезны:
if (import.meta.env.DEV) {
console.error(error);
}
В production предпочтительнее:
if (import.meta.env.PROD) {
sendToMonitoring(error);
}
Плохой пример:
const fetchUsers = async () => {
try {
const response =
await fetch('/api/users');
return response.json();
} catch (error) {
console.error(error);
throw error;
}
};
Проблема:
Опасный код:
catch (error) {
return [];
}
TanStack Query не узнает об ошибке.
logger.error({
query
});
Крупные объекты:
HTTP Client
↓
API Layer
↓
TanStack Query
↓
Global Error Handlers
↓
Normalization
↓
Monitoring
Все ошибки должны приводиться к общей структуре.
Основная обработка должна находиться в:
Консоль подходит только для development.
UI отображает ошибку пользователю, а logger отправляет техническую информацию в monitoring system.
Перед логированием данные должны очищаться от чувствительной информации.
Retry-цепочки не должны засорять monitoring дубликатами ошибок.