Логирование ошибок

Логирование ошибок — важная часть эксплуатации приложений, использующих TanStack Query. Библиотека активно управляет сетевыми запросами, кэшированием, повторными попытками, фоновыми обновлениями и синхронизацией состояния. При большом количестве асинхронных операций ошибки начинают появляться одновременно в разных слоях приложения:

  • query functions;
  • mutations;
  • background refetch;
  • retry-механизмы;
  • optimistic updates;
  • cache invalidation;
  • hydration и SSR;
  • offline-режимы.

Без централизованного логирования диагностика становится сложной. Ошибки теряются между компонентами, дублируются в консоли, попадают в UI несколько раз или вообще не фиксируются.


Источники ошибок в TanStack Query

Ошибки могут возникать на нескольких уровнях.

Ошибки query-функций

Наиболее распространённый источник — исключения внутри queryFn.

const fetchUsers = async () => {
    const response = await fetch('/api/users');

    if (!response.ok) {
        throw new Error('Ошибка загрузки пользователей');
    }

    return response.json();
};

Если queryFn выбрасывает исключение, TanStack Query:

  • помечает запрос как error;
  • сохраняет объект ошибки;
  • может запустить retry;
  • уведомляет observers;
  • вызывает обработчики ошибок.

Ошибки mutations

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 заключается в том, что ошибки часто связаны:

  • с валидацией;
  • конфликтами данных;
  • авторизацией;
  • optimistic update rollback;
  • повторной отправкой запросов.

Ошибки фонового refetch

Refetch может происходить:

  • при фокусе окна;
  • при восстановлении соединения;
  • по interval polling;
  • вручную;
  • после invalidation.

Ошибка в background refetch особенно опасна, потому что пользователь может продолжать видеть старые данные, не подозревая о проблеме.


Объект ошибки

TanStack Query не навязывает формат ошибки.

В error может находиться:

  • стандартный Error;
  • AxiosError;
  • кастомный объект;
  • ответ API;
  • строка;
  • неизвестное значение.

Пример:

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'
    };
};

Такой подход упрощает:

  • логирование;
  • аналитику;
  • интеграцию с monitoring systems;
  • отображение UI;
  • группировку ошибок.

Локальное логирование ошибок

Использование onError

Каждый 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 быстро приводят к проблемам:

  • дублирование кода;
  • разные форматы логов;
  • отсутствие централизованного контроля;
  • сложность отключения логирования;
  • несогласованность telemetry.

Глобальное логирование

QueryCache

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
            });
        }
    })
});

Преимущества:

  • единая точка логирования;
  • одинаковый формат;
  • удобная интеграция с Sentry;
  • контроль production telemetry.

MutationCache

Для 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
            });
        }
    })
});

Интеграция с системами мониторинга

Sentry

Одна из наиболее распространённых интеграций.

import * as Sentry from '@sentry/react';

const queryClient = new QueryClient({
    queryCache: new QueryCache({
        onError: (error, query) => {
            Sentry.captureException(error, {
                extra: {
                    queryKey: query.queryKey
                }
            });
        }
    })
});

Логирование mutation context

mutationCache: new MutationCache({
    onError: (error, variables, context, mutation) => {
        Sentry.captureException(error, {
            extra: {
                mutationKey: mutation.options.mutationKey,
                variables
            }
        });
    }
})

Контекст mutation особенно полезен при расследовании:

  • ошибок в формах;
  • конфликтов optimistic updates;
  • проблем сериализации данных;
  • race conditions.

Кастомный logger

logger API

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 logger

В production окружении консольные ошибки часто отключаются.

setLogger({
    log: () => {},

    warn: () => {},

    error: (error) => {
        sendToMonitoring(error);
    }
});

Структурированное логирование

Недостатки console.error

Простой вывод в консоль плохо масштабируется.

console.error(error);

Проблемы:

  • нет query metadata;
  • сложно фильтровать;
  • невозможно агрегировать;
  • нет correlation id;
  • неудобно анализировать.

Формирование структуры лога

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()
    });
};

Логирование retry-механизмов

Retry и шум логов

По умолчанию TanStack Query делает retry failed requests.

Если логировать каждую попытку:

retry: 3

то одна ошибка может попасть в лог четыре раза:

  • первая попытка;
  • retry #1;
  • retry #2;
  • retry #3.

Логирование только финальной ошибки

useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,

    retry: 3,

    onError: (error) => {
        console.error('Final query error', error);
    }
});

onError вызывается после завершения retry chain.


Логирование retry attempts

Иногда необходимо отслеживать все попытки.

retry: (failureCount, error) => {
    logger.warn({
        type: 'RETRY_ATTEMPT',
        failureCount,
        error
    });

    return failureCount < 3;
}

Разделение типов ошибок

Network errors

if (!navigator.onLine) {
    logger.warn({
        type: 'OFFLINE_ERROR'
    });
}

HTTP errors

if (response.status >= 500) {
    logger.error({
        type: 'SERVER_ERROR',
        status: response.status
    });
}

Validation errors

if (response.status === 422) {
    logger.warn({
        type: 'VALIDATION_ERROR'
    });
}

Unauthorized errors

if (response.status === 401) {
    logger.warn({
        type: 'AUTH_ERROR'
    });
}

Логирование query metadata

Query key

Query key — основной идентификатор запроса.

onError: (error, query) => {
    logger.error({
        queryKey: query.queryKey
    });
}

Query state

onError: (error, query) => {
    logger.error({
        state: query.state
    });
}

Поле query.state содержит:

  • timestamps;
  • fetchStatus;
  • errorUpdateCount;
  • dataUpdatedAt;
  • status.

Mutation variables

onError: (error, variables) => {
    logger.error({
        variables
    });
}

Безопасность логирования

Утечка чувствительных данных

Нельзя логировать:

  • access tokens;
  • refresh tokens;
  • passwords;
  • cookies;
  • банковские данные;
  • персональную информацию.

Опасный пример:

logger.error({
    headers: request.headers
});

Санитизация данных

const sanitize = (payload) => {
    return {
        ...payload,
        password: undefined,
        token: undefined
    };
};

Логирование optimistic updates

Ошибки rollback

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

Ошибки скрытых обновлений

Background refetch может ломаться незаметно.

useQuery({
    queryKey: ['notifications'],
    queryFn: fetchNotifications,
    refetchInterval: 10000,

    onError: (error) => {
        logger.warn({
            type: 'BACKGROUND_REFETCH_ERROR',
            error
        });
    }
});

Логирование offline-состояний

Network Mode

TanStack Query умеет работать в offline-first режимах.

useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    networkMode: 'offlineFirst'
});

Отслеживание offline ошибок

window.addEventListener('offline', () => {
    logger.warn({
        type: 'NETWORK_OFFLINE'
    });
});

Корреляция запросов

Correlation ID

Для сложных приложений полезно связывать запросы между собой.

const correlationId = crypto.randomUUID();

Добавление correlation ID

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();
};

Интеграция с Axios interceptors

Централизация HTTP-ошибок

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);
    }
);

Логирование времени выполнения

Performance logging

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 и логирование

React Query Devtools

Devtools помогают анализировать:

  • retries;
  • cache updates;
  • error states;
  • observers;
  • background fetching.

Пример подключения:

import {
    ReactQueryDevtools
} from '@tanstack/react-query-devtools';

<ReactQueryDevtools initialIsOpen={false} />

Построение единого error layer

Архитектурная схема

Крупные приложения обычно строят отдельный слой обработки ошибок:

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'
    });
}

Разделение development и production режимов

Development logging

Во время разработки полезны:

  • stack trace;
  • verbose logging;
  • payload dumping;
  • query state;
  • retry details.
if (import.meta.env.DEV) {
    console.error(error);
}

Production logging

В production предпочтительнее:

  • минимальный объём логов;
  • структурированные события;
  • удалённая отправка;
  • throttling;
  • batching.
if (import.meta.env.PROD) {
    sendToMonitoring(error);
}

Anti-patterns

Логирование внутри queryFn

Плохой пример:

const fetchUsers = async () => {
    try {
        const response =
            await fetch('/api/users');

        return response.json();
    } catch (error) {
        console.error(error);

        throw error;
    }
};

Проблема:

  • дублирование логов;
  • потеря централизации;
  • повторный лог при retry.

Подавление ошибок

Опасный код:

catch (error) {
    return [];
}

TanStack Query не узнает об ошибке.


Логирование огромных объектов

logger.error({
    query
});

Крупные объекты:

  • засоряют telemetry;
  • увеличивают размер логов;
  • ухудшают производительность;
  • могут содержать приватные данные.

Рекомендуемая архитектура

Базовая схема

HTTP Client
    ↓
API Layer
    ↓
TanStack Query
    ↓
Global Error Handlers
    ↓
Normalization
    ↓
Monitoring

Основные принципы

Единый формат ошибок

Все ошибки должны приводиться к общей структуре.


Централизованное логирование

Основная обработка должна находиться в:

  • QueryCache;
  • MutationCache;
  • HTTP interceptors.

Минимум console.log

Консоль подходит только для development.


Разделение UI и telemetry

UI отображает ошибку пользователю, а logger отправляет техническую информацию в monitoring system.


Санитизация данных

Перед логированием данные должны очищаться от чувствительной информации.


Контроль retry logging

Retry-цепочки не должны засорять monitoring дубликатами ошибок.