RTK Query строится вокруг идеи предсказуемого жизненного цикла
запроса. Ошибка является частью этого жизненного цикла и рассматривается
как полноценное состояние запроса, наряду с loading,
success и uninitialized.
Каждый endpoint в RTK Query может находиться в нескольких состояниях:
При возникновении ошибки RTK Query автоматически:
Тип ошибки зависит от используемого baseQuery.
При использовании fetchBaseQuery ошибки обычно имеют
одну из двух форм:
{
status: 404,
data: {
message: 'Not found'
}
}
Либо:
{
status: 'FETCH_ERROR',
error: 'TypeError: Failed to fetch'
}
Также возможны специальные типы ошибок:
{
status: 'PARSING_ERROR',
originalStatus: 200,
data: 'invalid json',
error: 'SyntaxError'
}
И:
{
status: 'TIMEOUT_ERROR',
error: 'Request timeout'
}
RTK Query предоставляет ошибки через hook.
Пример:
const {
data,
error,
isLoading,
isError
} = useGetUsersQuery()
Если запрос завершился неуспешно:
if (isError) {
console.log(error)
}
Статус ошибки обычно содержит HTTP-код.
Пример:
if (error?.status === 404) {
console.log('Ресурс не найден')
}
Обработка 401:
if (error?.status === 401) {
console.log('Необходима авторизация')
}
Обработка 500:
if (error?.status === 500) {
console.log('Ошибка сервера')
}
Ошибка может иметь разные структуры.
Небезопасный код:
console.log(error.data.message)
Безопасный вариант:
console.log(error?.data?.message)
Либо:
const message =
error?.data?.message ||
error?.error ||
'Unknown error'
Сетевые ошибки возникают, когда сервер недоступен или отсутствует соединение.
Пример:
{
status: 'FETCH_ERROR',
error: 'TypeError: Failed to fetch'
}
Проверка:
if (error?.status === 'FETCH_ERROR') {
console.log('Сетевая ошибка')
}
Если сервер вернул невалидный JSON, fetchBaseQuery
генерирует PARSING_ERROR.
Пример:
if (error?.status === 'PARSING_ERROR') {
console.log('Ошибка обработки ответа')
}
Mutation hooks возвращают Promise со специальной логикой RTK Query.
Без unwrap ошибки не выбрасываются через
catch.
Пример:
const [createUser] = useCreateUserMutation()
const handleCreate = async () => {
try {
const result = await createUser({
name: 'Alex'
}).unwrap()
console.log(result)
} catch (error) {
console.log(error)
}
}
unwrap():
try/catch.Mutation hook также предоставляет объект ошибки.
Пример:
const [
updateUser,
{
error,
isError,
isLoading
}
] = useUpdateUserMutation()
Проверка:
if (isError) {
console.log(error)
}
Mutation удобно комбинировать с async/await.
Пример:
const handleSubmit = async () => {
try {
await updateUser(data).unwrap()
console.log('Успешно')
} catch (error) {
console.log('Ошибка')
}
}
Крупные приложения редко обрабатывают ошибки прямо внутри компонентов.
Распространённая практика — создание кастомного
baseQuery.
Пример:
import {
fetchBaseQuery
} from '@reduxjs/toolkit/query/react'
const baseQuery = fetchBaseQuery({
baseUrl: '/api'
})
const baseQueryWithErrorHandler = async (
args,
api,
extraOptions
) => {
const result = await baseQuery(
args,
api,
extraOptions
)
if (result.error) {
console.log(result.error)
}
return result
}
Частый сценарий — автоматический logout.
Пример:
const baseQueryWithAuth = async (
args,
api,
extraOptions
) => {
const result = await baseQuery(
args,
api,
extraOptions
)
if (result.error?.status === 401) {
api.dispatch(logout())
}
return result
}
RTK Query часто используется вместе с refresh token.
Схема работы:
const baseQuery = fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) {
headers.set(
'authorization',
`Bearer ${token}`
)
}
return headers
}
})
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) {
api.dispatch(
setToken(refreshResult.data.token)
)
result = await baseQuery(
args,
api,
extraOptions
)
} else {
api.dispatch(logout())
}
}
return result
}
RTK Query поддерживает автоматические повторные запросы.
Используется utility retry.
Пример:
import {
retry
} from '@reduxjs/toolkit/query/react'
const staggeredBaseQuery = retry(
fetchBaseQuery({
baseUrl: '/api'
}),
{
maxRetries: 5
}
)
RTK Query повторяет запросы:
Retry особенно полезен:
Пример:
const customBaseQuery = retry(
fetchBaseQuery({
baseUrl: '/api'
}),
{
maxRetries: 3
}
)
Дополнительно можно проверять тип ошибки:
const baseQueryWithConditionalRetry =
retry(
async (args, api, extraOptions) => {
const result = await baseQuery(
args,
api,
extraOptions
)
if (
result.error?.status === 400
) {
retry.fail(result.error)
}
return result
}
)
Ошибка может возникать не только в HTTP-запросе.
Пример:
transformResponse: (response) => {
return response.data.items
}
Если response.data отсутствует:
Cannot read properties of undefined
Безопасный вариант:
transformResponse: (response) => {
return response?.data?.items || []
}
RTK Query позволяет модифицировать объект ошибки.
Пример:
transformErrorResponse: (
response
) => {
return {
status: response.status,
message:
response.data?.message ||
'Server error'
}
}
Теперь ошибка будет иметь форму:
{
status: 500,
message: 'Server error'
}
Бэкенды часто возвращают разные структуры ошибок.
Например:
{
error: 'Validation error'
}
Или:
{
message: 'Invalid credentials'
}
Нормализация решает проблему несогласованных API.
Пример:
transformErrorResponse: (
response
) => {
return {
status: response.status,
message:
response.data?.message ||
response.data?.error ||
'Unknown error'
}
}
RTK Query не заменяет React Error Boundaries.
Важно понимать различия:
| Тип ошибки | Error Boundary | RTK Query |
|---|---|---|
| HTTP ошибки | Нет | Да |
| Network ошибки | Нет | Да |
| Render ошибки | Да | Нет |
| Runtime ошибки | Да | Нет |
RTK Query предоставляет специальный флаг:
const {
isError
} = useGetPostsQuery()
Пример UI:
if (isError) {
return <div>Error</div>
}
Многие backend API возвращают полезную информацию.
Пример:
{
message: 'Email already exists'
}
Использование:
if (error?.data?.message) {
console.log(error.data.message)
}
Сервер может возвращать ошибки формы.
Пример:
{
errors: {
email: 'Invalid email',
password: 'Too short'
}
}
Обработка:
const validationErrors =
error?.data?.errors
Использование:
<input />
<span>
{validationErrors?.email}
</span>
RTK Query поддерживает собственную реализацию запросов через
queryFn.
Пример:
getUser: builder.query({
async queryFn(id) {
try {
const response =
await customApi.getUser(id)
return {
data: response
}
} catch (error) {
return {
error: {
status: 500,
data: error
}
}
}
}
})
При интеграции с async logic иногда используется
rejectWithValue.
Пример:
return {
error: {
status: 400,
data: {
message: 'Validation failed'
}
}
}
В TypeScript ошибки RTK Query имеют union-тип.
Пример:
FetchBaseQueryError
| SerializedError
Поэтому часто используются type guards.
Пример:
if ('status' in error) {
console.log(error.status)
}
Некоторые ошибки являются runtime-ошибками.
Пример:
{
name: 'TypeError',
message: 'Failed to fetch'
}
Это объект типа SerializedError.
Повторяющийся код:
if (error?.status === 401)
лучше выносить:
export const isUnauthorized =
(error) =>
error?.status === 401
Использование:
if (isUnauthorized(error)) {
logout()
}
Ошибки RTK Query можно перехватывать через middleware.
Пример:
const errorLogger =
() => (next) => (action) => {
if (action.error) {
console.log(action.error)
}
return next(action)
}
Часто ошибки отображаются через toast.
Пример:
try {
await login(data).unwrap()
} catch (error) {
toast.error(
error?.data?.message
)
}
Важно различать:
| Тип | Диапазон |
|---|---|
| Client errors | 400–499 |
| Server errors | 500–599 |
Пример:
if (error.status >= 500) {
console.log('Проблема сервера')
}
Сырые backend-сообщения редко подходят для UI.
Нежелательно:
"SQLSTATE[23505]"
Лучше:
const getErrorMessage = (
error
) => {
switch (error?.status) {
case 401:
return 'Необходим вход'
case 403:
return 'Нет доступа'
case 404:
return 'Ресурс не найден'
default:
return 'Произошла ошибка'
}
}
RTK Query умеет отменять запросы.
Пример:
const promise =
dispatch(api.endpoints.getUsers.initiate())
promise.abort()
После отмены запрос получает aborted state.
Обычно отменённые запросы не считаются ошибками UI.
Пример:
if (error?.name === 'AbortError') {
return
}
Polling-запросы требуют особого подхода.
Нежелательно показывать popup при каждой ошибке polling.
Распространённая стратегия:
При optimistic update mutation может завершиться ошибкой.
Пример:
async onQueryStarted(
arg,
{ dispatch, queryFulfilled }
) {
const patchResult =
dispatch(
api.util.updateQueryData(
'getPosts',
undefined,
(draft) => {
draft.push(arg)
}
)
)
try {
await queryFulfilled
} catch {
patchResult.undo()
}
}
Если mutation завершится ошибкой, изменения откатываются.
В production ошибки обычно отправляются:
Пример:
if (result.error) {
Sentry.captureException(
result.error
)
}
Типичная архитектура:
baseQueryWithReauthtransformErrorResponsetoast middlewareТакой подход обеспечивает: