Обработка ошибок на сервере

Взаимодействие с сервером в RTK Query строится вокруг единого механизма выполнения запросов через baseQuery, где ошибки представляют собой структурированные результаты выполнения запроса. Основное разделение происходит на несколько классов: сетевые ошибки, HTTP-ошибки, ошибки парсинга ответа и прикладные ошибки, возвращаемые сервером в теле ответа.

Сетевые ошибки возникают на уровне транспорта: отсутствие соединения, таймауты, блокировка CORS, сбои DNS. Такие ошибки обычно не содержат HTTP-статуса и формируются как объект error с полем status: 'FETCH_ERROR'.

HTTP-ошибки формируются при наличии ответа сервера с кодом вне диапазона 200–299. В RTK Query они фиксируются как { status: number, data: unknown }.

Ошибки формата данных появляются при невозможности корректно разобрать ответ (например, при несоответствии JSON-формата). В таких случаях возвращается PARSING_ERROR.

Прикладные серверные ошибки — это бизнес-ошибки, когда запрос технически успешен, но сервер возвращает структуру вида { error: string, code: string } или аналогичную. Такие ошибки требуют явного извлечения из data.


Базовая обработка ошибок через fetchBaseQuery

fetchBaseQuery предоставляет стандартную реализацию обработки HTTP-запросов, где ошибки автоматически нормализуются в единый формат.

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

const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({
    baseUrl: '/api'
  }),
  endpoints: (builder) => ({
    getUser: builder.query({
      query: (id) => `user/${id}`
    })
  })
});

При таком подходе ошибка автоматически попадает в error поля результата запроса и доступна через isError, error, isLoading.


Структура объекта ошибки

RTK Query унифицирует ошибки в следующую структуру:

  • status: HTTP-код, строка 'FETCH_ERROR', 'PARSING_ERROR' или 'CUSTOM_ERROR'
  • data: тело ответа сервера (если доступно)
  • error: текстовое описание
  • originalStatus: исходный HTTP-статус (в некоторых случаях)

Пример:

{
  status: 401,
  data: {
    message: 'Unauthorized',
    code: 'AUTH_EXPIRED'
  }
}

Преобразование ошибок через transformErrorResponse

Для серверов с нестандартным форматом ответов применяется transformErrorResponse. Этот механизм позволяет привести любую серверную ошибку к унифицированной форме.

getUser: builder.query({
  query: (id) => `user/${id}`,
  transformErrorResponse: (response) => {
    return {
      status: response.status,
      message: response.data?.message || 'Unknown error',
      code: response.data?.code
    };
  }
});

После преобразования error в состоянии запроса содержит уже нормализованную структуру, удобную для логики приложения.


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

Мутации и запросы в RTK Query позволяют перехватывать ошибки через onQueryStarted. Этот механизм используется для побочных эффектов и контроля состояния при провале запроса.

addUser: builder.mutation({
  query: (user) => ({
    url: 'user',
    method: 'POST',
    body: user
  }),
  async onQueryStarted(arg, { queryFulfilled }) {
    try {
      await queryFulfilled;
    } catch (err) {
      console.log('Ошибка запроса:', err.error);
    }
  }
});

queryFulfilled возвращает промис, который отклоняется при любой серверной или сетевой ошибке.


Разбор HTTP-кодов и централизованная логика

Серверные ошибки часто требуют централизованного сопоставления кодов с действиями. RTK Query позволяет реализовать это через кастомный baseQuery.

const baseQuery = fetchBaseQuery({
  baseUrl: '/api'
});

const baseQueryWithErrorMapping = async (args, api, extraOptions) => {
  const result = await baseQuery(args, api, extraOptions);

  if (result.error) {
    if (result.error.status === 401) {
      // обработка авторизации
    }

    if (result.error.status === 500) {
      // логирование серверной ошибки
    }
  }

  return result;
};

Такой слой позволяет реализовать глобальную стратегию обработки ошибок без дублирования в каждом endpoint.


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

Мутации требуют особого подхода, так как они часто сопровождаются оптимистическими обновлениями. При ошибке необходимо откатить локальное состояние.

updateUser: builder.mutation({
  query: (user) => ({
    url: `user/${user.id}`,
    method: 'PUT',
    body: user
  }),
  async onQueryStarted(user, { dispatch, queryFulfilled }) {
    const patch = dispatch(
      api.util.updateQueryData('getUser', user.id, (draft) => {
        Object.assign(draft, user);
      })
    );

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

Ошибка сервера здесь приводит к восстановлению предыдущего состояния кэша, что предотвращает рассинхронизацию UI и backend.


Обработка ошибок парсинга ответа

Ошибки парсинга возникают при некорректном JSON или неожиданной структуре ответа. RTK Query фиксирует их как PARSING_ERROR.

baseQuery: fetchBaseQuery({
  baseUrl: '/api',
  responseHandler: async (response) => {
    const text = await response.text();
    try {
      return JSON.parse(text);
    } catch {
      throw new Error('Invalid JSON');
    }
  }
});

Подобная логика позволяет дополнительно контролировать корректность данных до попадания в кэш.


Унификация серверных ошибок

Многие API возвращают ошибки в разных форматах:

{ "error": "message" }

или

{ "message": "error", "errors": [] }

Для унификации используется слой адаптации:

const normalizeError = (data) => {
  if (!data) return 'Unknown error';

  if (typeof data === 'string') return data;

  return data.message || data.error || 'Unknown error';
};

И интеграция в transformErrorResponse:

transformErrorResponse: (response) => ({
  status: response.status,
  message: normalizeError(response.data)
})

Поведение при сетевых сбоях и офлайн-режиме

Сетевые ошибки фиксируются без HTTP-статуса и требуют отдельной обработки. В RTK Query они выглядят как:

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

Для таких случаев часто применяется проверка:

if (error?.status === 'FETCH_ERROR') {
  // отсутствие сети
}

При работе в офлайн-режиме важно учитывать, что RTK Query продолжает хранить кэшированные данные, а повторные запросы автоматически выполняются при восстановлении соединения, если активна подписка на компонент.


Повторные попытки запросов и серверные ошибки

Механизм retry позволяет автоматически повторять запросы при временных сбоях сервера.

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

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

Повторные попытки особенно полезны при обработке 502, 503, 504 ошибок, когда сервер нестабилен.


Интеграция с пользовательскими кодами ошибок

Многие серверные API используют доменные коды ошибок:

{
  "code": "USER_BLOCKED",
  "message": "User is blocked"
}

Такие коды удобно обрабатывать через централизованную карту:

const errorHandlers = {
  USER_BLOCKED: () => {
    // блокировка интерфейса
  },
  AUTH_EXPIRED: () => {
    // очистка сессии
  }
};

И вызов при получении ошибки:

if (error?.data?.code) {
  errorHandlers[error.data.code]?.();
}

Ошибки в асинхронных цепочках RTK Query

RTK Query использует промисы для выполнения запросов, поэтому ошибки могут быть перехвачены через unwrap.

try {
  await dispatch(api.endpoints.getUser.initiate(id)).unwrap();
} catch (err) {
  console.log(err);
}

unwrap переводит результат в чистый промис, где ошибка пробрасывается через throw, что упрощает обработку в логике middleware или saga-подобных сценариях.


Особенности сериализации ошибок в store

RTK Query хранит ошибки в Redux store, поэтому они должны быть сериализуемыми. Нежелательно помещать туда:

  • экземпляры классов Error
  • функции
  • циклические структуры

Рекомендуемый формат — plain object:

{
  status: number | string,
  message: string,
  code?: string
}

Обработка конкурентных ошибок и устаревших запросов

При параллельных запросах возможна ситуация, когда ошибка относится к уже устаревшему запросу. RTK Query использует внутренние идентификаторы requestId, позволяющие различать состояния.

async onQueryStarted(arg, { requestId, queryFulfilled }) {
  try {
    await queryFulfilled;
  } catch (e) {
    // ошибка относится к конкретному requestId
  }
}

Это предотвращает влияние устаревших ошибок на актуальное состояние интерфейса.


Обобщение поведения серверных ошибок в кэш-слое

RTK Query сохраняет ошибки в кэше вместе с данными запроса. Это означает, что повторный рендер компонента может использовать уже сохранённое состояние ошибки без повторного запроса. Такая модель делает поведение предсказуемым: ошибка становится частью состояния данных, а не временным побочным эффектом вызова API.