В 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 не выбрасывает исключения в привычном смысле — ошибки возвращаются как часть результата запроса.
В реальных приложениях ошибки делятся на несколько устойчивых категорий:
HTTP-ошибки
Сетевые ошибки
Ошибки отмены
abortControllerБизнес-ошибки
baseQueryProduction-уровень обработки начинается с нормализации ошибок в
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.
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 это используется для:
Мутации требуют отдельного внимания из-за 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()
}
}
Это критично при работе с медленными или нестабильными сетями.
Централизация ошибок позволяет интегрировать логирование и аналитические системы.
const rtkQueryErrorLogger = () => (next) => (action) => {
if (action?.type?.endsWith('rejected')) {
console.error('RTK Query error:', action.error)
}
return next(action)
}
Production расширяет это до:
Сетевые ошибки требуют автоматического повторения.
import { retry } from '@reduxjs/toolkit/query'
const baseQuery = retry(fetchBaseQuery({ baseUrl: '/api' }), {
maxRetries: 3
})
Продвинутая стратегия:
FETCH_ERRORconst baseQuery = retry(fetchBaseQuery({ baseUrl: '/api' }), {
retryCondition: (error) => {
return error.status === 'FETCH_ERROR'
}
})
Один из ключевых 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 при истечении токена.
RTK Query предоставляет три состояния:
isLoadingisErrorerrorПример production-логики:
const { data, error, isError, isLoading } = useGetUserQuery(id)
if (isLoading) return <Skeleton />
if (isError) {
return (
<ErrorBlock
message={error?.data?.message || 'Unexpected error'}
/>
)
}
Важно различать:
Сервер часто возвращает структурированные ошибки:
{
"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-валидации.
Production-системы требуют наблюдаемости:
Пример интеграции:
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 }Типовая модель:
{
type: 'VALIDATION' | 'NETWORK' | 'AUTH' | 'SERVER',
message: string,
code?: number
}
Такой слой позволяет изолировать RTK Query от UI-логики и упростить миграции backend API без каскадных изменений на фронтенде.