RTK Query предоставляет встроенную систему обработки ошибок, тесно
связанную с TypeScript-типами. В отличие от ручной работы с
fetch, библиотека формирует предсказуемую структуру ошибок,
которую можно безопасно анализировать как в компонентах, так и внутри
baseQuery, middleware и lifecycle-обработчиков.
Основные типы ошибок в RTK Query:
queryFn.Корректная типизация позволяет:
status;data;any;При использовании fetchBaseQuery RTK Query возвращает
ошибки типа:
FetchBaseQueryError
Импорт:
import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'
Структура:
type FetchBaseQueryError =
| {
status: number
data: unknown
}
| {
status: 'FETCH_ERROR'
data?: undefined
error: string
}
| {
status: 'PARSING_ERROR'
originalStatus: number
data: string
error: string
}
| {
status: 'TIMEOUT_ERROR'
error: string
}
| {
status: 'CUSTOM_ERROR'
data?: unknown
error: string
}
RTK Query использует discriminated unions, поэтому TypeScript умеет
автоматически сужать типы через проверку status.
Наиболее распространённый вариант:
{
status: 404,
data: {
message: 'User not found'
}
}
Пример endpoint:
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
endpoints: (builder) => ({
getUser: builder.query<User, number>({
query: (id) => `/users/${id}`
})
})
})
Использование в компоненте:
const { error } = api.useGetUserQuery(1)
Тип:
error: FetchBaseQueryError | SerializedError | undefined
Без narrowing TypeScript не позволит безопасно читать свойства.
Неправильно:
if (error) {
console.log(error.status)
}
Ошибка:
Property 'status' does not exist
Причина — SerializedError.
Правильно:
if (error && 'status' in error) {
console.log(error.status)
}
Теперь TypeScript понимает:
error: FetchBaseQueryError
RTK Query может возвращать:
SerializedError
Импорт:
import type { SerializedError } from '@reduxjs/toolkit'
Структура:
interface SerializedError {
name?: string
message?: string
stack?: string
code?: string
}
Такие ошибки появляются:
throw new Error().Практически всегда создают helper:
import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'
export function isFetchBaseQueryError(
error: unknown
): error is FetchBaseQueryError {
return typeof error === 'object' &&
error != null &&
'status' in error
}
Использование:
if (isFetchBaseQueryError(error)) {
console.log(error.status)
}
Дополнительный guard:
import type { SerializedError } from '@reduxjs/toolkit'
export function isSerializedError(
error: unknown
): error is SerializedError {
return typeof error === 'object' &&
error != null &&
'message' in error
}
Использование:
if (isSerializedError(error)) {
console.log(error.message)
}
Типичный production-подход:
if (error) {
if (isFetchBaseQueryError(error)) {
if (typeof error.status === 'number') {
console.log('HTTP Error', error.status)
} else {
console.log('RTKQ Error', error.status)
}
} else if (isSerializedError(error)) {
console.log(error.message)
}
}
По умолчанию:
data: unknown
Это сделано намеренно, потому что сервер может вернуть любую структуру.
Типичный API error response:
interface ApiError {
message: string
errors?: Record<string, string[]>
}
Извлечение:
if (
isFetchBaseQueryError(error) &&
typeof error.status === 'number'
) {
const data = error.data as ApiError
console.log(data.message)
}
Повторяющиеся приведения типов обычно выносят:
interface ApiError {
message: string
}
function getErrorMessage(error: unknown): string {
if (isFetchBaseQueryError(error)) {
const data = error.data as ApiError
return data.message
}
if (isSerializedError(error)) {
return error.message ?? 'Unknown error'
}
return 'Unknown error'
}
Использование:
const message = getErrorMessage(error)
Одна из главных возможностей RTK Query — создание собственного
baseQuery.
Пример:
type CustomError = {
status: number
message: string
}
const customBaseQuery: BaseQueryFn<
string,
unknown,
CustomError
> = async (url) => {
try {
const response = await fetch(url)
if (!response.ok) {
return {
error: {
status: response.status,
message: 'Request failed'
}
}
}
const data = await response.json()
return { data }
} catch {
return {
error: {
status: 500,
message: 'Network error'
}
}
}
}
Третий generic параметр:
BaseQueryFn<
Args,
Result,
Error
>
Именно он определяет тип ошибки.
После указания кастомной ошибки:
const api = createApi({
reducerPath: 'api',
baseQuery: customBaseQuery,
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({
query: () => '/posts'
})
})
})
В компоненте:
const { error } = api.useGetPostsQuery()
Тип:
error: CustomError | undefined
Это полностью исключает необходимость в
FetchBaseQueryError.
queryFn позволяет вручную реализовывать запросы.
Пример:
getUser: builder.query<User, number>({
async queryFn(id) {
try {
const response = await fetch(`/users/${id}`)
if (!response.ok) {
return {
error: {
status: response.status,
message: 'User error'
}
}
}
const data = await response.json()
return { data }
} catch {
return {
error: {
status: 500,
message: 'Network error'
}
}
}
}
})
RTK Query автоматически выводит тип ошибки из
baseQuery.
При использовании async thunk совместно с RTK Query важно правильно типизировать reject values.
Пример:
interface ValidationError {
message: string
fields: Record<string, string>
}
Thunk:
createAsyncThunk<
User,
UserInput,
{
rejectValue: ValidationError
}
>(
'users/create',
async (data, { rejectWithValue }) => {
const response = await fetch('/users', {
method: 'POST',
body: JSON.stringify(data)
})
if (!response.ok) {
return rejectWithValue(await response.json())
}
return response.json()
}
)
Теперь:
action.payload
будет иметь тип:
ValidationError
RTK Query поддерживает:
.unwrap()
Пример:
try {
const result = await createUser(data).unwrap()
} catch (error) {
console.log(error)
}
Тип ошибки зависит от baseQuery.
При fetchBaseQuery:
FetchBaseQueryError | SerializedError
Можно создать helper:
async function safeRequest<T>(
promise: Promise<T>
): Promise<[T | null, unknown]> {
try {
const data = await promise
return [data, null]
} catch (error) {
return [null, error]
}
}
Использование:
const [result, error] = await safeRequest(
createUser(data).unwrap()
)
Ошибки могут возникать внутри:
transformResponse
Пример:
transformResponse: (response: RawUser): User => {
if (!response.id) {
throw new Error('Invalid user')
}
return {
id: response.id,
name: response.name
}
}
Такие ошибки превращаются в:
SerializedError
RTK Query позволяет переопределять успешные статусы.
Пример:
query: () => ({
url: '/login',
validateStatus: (response, body) => {
return response.status === 200 && !body.error
}
})
Если функция возвращает false, RTK Query формирует
ошибку.
Ошибка сети:
{
status: 'FETCH_ERROR',
error: 'TypeError: Failed to fetch'
}
Проверка:
if (
isFetchBaseQueryError(error) &&
error.status === 'FETCH_ERROR'
) {
console.log(error.error)
}
Возникает при ошибке JSON parsing.
Пример:
if (
isFetchBaseQueryError(error) &&
error.status === 'PARSING_ERROR'
) {
console.log(error.originalStatus)
console.log(error.data)
}
При использовании timeout:
fetchBaseQuery({
baseUrl: '/api',
timeout: 5000
})
Возможная ошибка:
{
status: 'TIMEOUT_ERROR',
error: 'Timed out'
}
Проверка:
if (
isFetchBaseQueryError(error) &&
error.status === 'TIMEOUT_ERROR'
) {
console.log('Request timeout')
}
Для ручных ошибок:
return {
error: {
status: 'CUSTOM_ERROR',
error: 'Token expired'
}
}
Проверка:
if (
isFetchBaseQueryError(error) &&
error.status === 'CUSTOM_ERROR'
) {
console.log(error.error)
}
Часто создают общий mapper:
export function mapError(error: unknown): string {
if (isFetchBaseQueryError(error)) {
if (typeof error.status === 'number') {
const data = error.data as ApiError
return data.message
}
switch (error.status) {
case 'FETCH_ERROR':
return 'Network error'
case 'PARSING_ERROR':
return 'Response parsing error'
case 'TIMEOUT_ERROR':
return 'Request timeout'
case 'CUSTOM_ERROR':
return error.error
default:
return 'Unknown error'
}
}
if (isSerializedError(error)) {
return error.message ?? 'Runtime error'
}
return 'Unknown error'
}
RTK Query генерирует rejected actions.
Пример middleware:
import { isRejectedWithValue } from '@reduxjs/toolkit'
export const errorMiddleware =
() => (next) => (action) => {
if (isRejectedWithValue(action)) {
console.log(action.payload)
}
return next(action)
}
Тип payload зависит от reject value.
Пример:
listenerMiddleware.startListening({
matcher: api.endpoints.login.matchRejected,
effect: async (action) => {
console.log(action.payload)
}
})
RTK Query предоставляет типизированные matcher’ы.
Каждый endpoint имеет:
matchPending
matchFulfilled
matchRejected
Пример:
if (api.endpoints.login.matchRejected(action)) {
console.log(action.payload)
}
TypeScript автоматически сузит тип action.
Ошибки особенно важны при optimistic update.
Пример:
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
api.util.updateQueryData(
'getPosts',
undefined,
(draft) => {
draft.push(arg)
}
)
)
try {
await queryFulfilled
} catch {
patchResult.undo()
}
}
queryFulfilled может выбросить:
{
error: FetchBaseQueryError | SerializedError
}
Можно явно типизировать:
try {
const { data } = await queryFulfilled
} catch (error) {
console.log(error)
}
Либо:
try {
await queryFulfilled
} catch (error: unknown) {
if (isFetchBaseQueryError(error)) {
console.log(error.status)
}
}
Многие проекты создают единый контракт ошибок:
interface ApiErrorResponse {
success: false
message: string
code: string
}
Тогда любой endpoint возвращает одинаковую структуру.
Преимущества:
as.Продвинутый вариант:
function extractError<T>(
error: unknown
): T | null {
if (
isFetchBaseQueryError(error) &&
typeof error.status === 'number'
) {
return error.data as T
}
return null
}
Использование:
const apiError = extractError<ApiError>(error)
Современный TypeScript трактует catch как
unknown.
Правильно:
catch (error: unknown) {
if (isFetchBaseQueryError(error)) {
console.log(error.status)
}
}
Неправильно:
catch (error: any)
any уничтожает типовую безопасность.
Пример:
type AppError =
| FetchBaseQueryError
| SerializedError
Helper:
function logError(error: AppError) {
if ('status' in error) {
console.log(error.status)
} else {
console.log(error.message)
}
}
Иногда создают собственный union:
type ExtendedError =
| FetchBaseQueryError
| {
status: 'VALIDATION_ERROR'
fields: Record<string, string>
}
Проверка:
if (error.status === 'VALIDATION_ERROR') {
console.log(error.fields)
}
Крупные проекты обычно строят систему из:
Такой подход позволяет RTK Query превращать обработку ошибок из
набора if-проверок в полноценную типобезопасную архитектуру
взаимодействия с сервером.