Обработка ошибок в production

В Redux Toolkit RTK Query ошибки формируются на уровне baseQuery и стандартизируются в объект, который попадает в состояние запроса и в результат хука. В production-проектах критично понимать форму ошибки, потому что именно она определяет стратегию восстановления, повторов и пользовательской реакции.

Типовая структура ошибки:

{
  status: 404,
  data: {
    message: "Not Found"
  }
}

или при сетевой ошибке:

{
  status: "FETCH_ERROR",
  error: "TypeError: Failed to fetch"
}

или при отмене запроса:

{
  status: "TIMEOUT_ERROR",
  error: "Aborted"
}

Ключевой момент заключается в том, что RTK Query не выбрасывает исключения в привычном смысле — ошибки возвращаются как часть результата запроса.


Типы ошибок в production-сценариях

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

HTTP-ошибки

  • 400 — некорректные данные
  • 401 — неавторизован
  • 403 — доступ запрещён
  • 404 — ресурс не найден
  • 500+ — серверные сбои

Сетевые ошибки

  • отсутствие интернета
  • DNS сбои
  • CORS проблемы

Ошибки отмены

  • переключение маршрута
  • повторный запрос с тем же ключом
  • manual abort через abortController

Бизнес-ошибки

  • валидатор на сервере
  • доменные ограничения
  • конфликт состояния

Унификация ошибок через baseQuery

Production-уровень обработки начинается с нормализации ошибок в baseQuery. Это позволяет не размазывать логику по компонентам.

Пример расширенного fetchBaseQuery:

import { fetchBaseQuery } from '@reduxjs/toolkit/query/react'

const rawBaseQuery = fetchBaseQuery({
  baseUrl: '/api',
  credentials: 'include',
})

export const baseQuery = async (args, api, extraOptions) => {
  const result = await rawBaseQuery(args, api, extraOptions)

  if (result.error) {
    const normalized = normalizeError(result.error)

    return {
      error: normalized
    }
  }

  return result
}

function normalizeError(error) {
  if (error.status === 'FETCH_ERROR') {
    return {
      type: 'NETWORK',
      message: 'Network unavailable',
      raw: error
    }
  }

  if (typeof error.status === 'number') {
    return {
      type: 'HTTP',
      status: error.status,
      message: error.data?.message || 'Server error',
      raw: error
    }
  }

  return {
    type: 'UNKNOWN',
    message: 'Unexpected error',
    raw: error
  }
}

Такой подход позволяет унифицировать обработку на уровне UI и middleware.


Обработка ошибок в endpoints

RTK Query позволяет перехватывать ошибки на уровне endpoint через transformResponse и transformErrorResponse.

getUser: builder.query({
  query: (id) => `/users/${id}`,

  transformErrorResponse: (response) => {
    return {
      message: response.data?.message || 'Failed to load user',
      code: response.status
    }
  }
})

В production это используется для:

  • приведения ошибок к единому формату
  • скрытия внутренних серверных деталей
  • подготовки данных для UI

Ошибки в мутациях и откат состояния

Мутации требуют отдельного внимания из-за side-effect логики.

updateUser: builder.mutation({
  query: (data) => ({
    url: `/users/${data.id}`,
    method: 'PUT',
    body: data
  }),

  async onQueryStarted(arg, { dispatch, queryFulfilled }) {
    try {
      const result = await queryFulfilled
    } catch (err) {
      console.log('Mutation failed', err)
    }
  }
})

В production часто применяется optimistic update:

async onQueryStarted(arg, { dispatch, queryFulfilled, getState }) {
  const patch = dispatch(
    api.util.updateQueryData('getUser', arg.id, (draft) => {
      Object.assign(draft, arg)
    })
  )

  try {
    await queryFulfilled
  } catch {
    patch.undo()
  }
}

Это критично при работе с медленными или нестабильными сетями.


Глобальная обработка ошибок через middleware

Централизация ошибок позволяет интегрировать логирование и аналитические системы.

const rtkQueryErrorLogger = () => (next) => (action) => {
  if (action?.type?.endsWith('rejected')) {
    console.error('RTK Query error:', action.error)
  }

  return next(action)
}

Production расширяет это до:

  • отправки в Sentry
  • логирования в backend observability
  • трассировки пользовательских сессий

Повтор запросов и retry-стратегии

Сетевые ошибки требуют автоматического повторения.

import { retry } from '@reduxjs/toolkit/query'

const baseQuery = retry(fetchBaseQuery({ baseUrl: '/api' }), {
  maxRetries: 3
})

Продвинутая стратегия:

  • retry только для FETCH_ERROR
  • exponential backoff
  • исключение 4xx ошибок
const baseQuery = retry(fetchBaseQuery({ baseUrl: '/api' }), {
  retryCondition: (error) => {
    return error.status === 'FETCH_ERROR'
  }
})

Обработка 401 и обновление токена

Один из ключевых production-паттернов — автоматическое обновление access token.

const baseQueryWithReauth = async (args, api, extraOptions) => {
  let result = await baseQuery(args, api, extraOptions)

  if (result.error?.status === 401) {
    const refreshResult = await baseQuery(
      '/auth/refresh',
      api,
      extraOptions
    )

    if (refreshResult.data) {
      result = await baseQuery(args, api, extraOptions)
    } else {
      // logout logic
    }
  }

  return result
}

Это предотвращает деградацию UX при истечении токена.


Ошибки в UI-слое

RTK Query предоставляет три состояния:

  • isLoading
  • isError
  • error

Пример production-логики:

const { data, error, isError, isLoading } = useGetUserQuery(id)

if (isLoading) return <Skeleton />

if (isError) {
  return (
    <ErrorBlock
      message={error?.data?.message || 'Unexpected error'}
    />
  )
}

Важно различать:

  • техническую ошибку (network)
  • бизнес-ошибку (validation)
  • отсутствие данных (404)

Валидационные ошибки и серверные схемы

Сервер часто возвращает структурированные ошибки:

{
  "errors": {
    "email": "Invalid email format",
    "password": "Too short"
  }
}

Нормализация:

function normalizeValidationError(error) {
  const errors = error.data?.errors

  if (!errors) return null

  return Object.entries(errors).map(([field, message]) => ({
    field,
    message
  }))
}

Это используется для форм и inline-валидации.


Логирование и observability

Production-системы требуют наблюдаемости:

  • request id correlation
  • user session tracking
  • latency measurement
  • error grouping

Пример интеграции:

const baseQuery = async (args, api, extraOptions) => {
  const start = Date.now()

  const result = await rawBaseQuery(args, api, extraOptions)

  const duration = Date.now() - start

  if (result.error) {
    sendToLogger({
      url: args.url,
      duration,
      error: result.error
    })
  }

  return result
}

Обработка отменённых запросов

RTK Query активно использует abort механизмы при:

  • смене параметров
  • размонтировании компонента
  • дублирующих запросах

Отмена выглядит как ошибка с типом:

status: "PARSING_ERROR" | "CANCELLED"

Production-логика обычно игнорирует такие ошибки:

if (error.status === 'CANCELLED') {
  return
}

Консистентность ошибок в масштабируемых приложениях

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

  • всегда возвращать { type, message, code }
  • скрывать raw server response
  • централизовать mapping HTTP → domain error
  • отделять UI error state от логической ошибки

Типовая модель:

{
  type: 'VALIDATION' | 'NETWORK' | 'AUTH' | 'SERVER',
  message: string,
  code?: number
}

Такой слой позволяет изолировать RTK Query от UI-логики и упростить миграции backend API без каскадных изменений на фронтенде.