Skip to content

Commit 0241927

Browse files
Shrugsymsutkowskiphryneas
authored
📝 Add RTKQ typescript error handling section (#1871)
* 📝 Add RTKQ typescript error handling section * Apply suggestions from code review Co-authored-by: Matt Sutkowski <msutkowski@gmail.com> Co-authored-by: Lenz Weber <mail@lenzw.de> * 📝 Update FetchBaseQueryError docstring Co-authored-by: Matt Sutkowski <msutkowski@gmail.com> Co-authored-by: Lenz Weber <mail@lenzw.de>
1 parent 13ee4c5 commit 0241927

File tree

2 files changed

+184
-3
lines changed

2 files changed

+184
-3
lines changed

docs/rtk-query/usage-with-typescript.mdx

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,3 +497,184 @@ function MaybePost({ id }: { id?: number }) {
497497
return <div>...</div>
498498
}
499499
```
500+
501+
## Type safe error handling
502+
503+
When an error is gracefully provided from a [`base query`](./api/createApi.mdx#baseQuery), RTK query will provide the error
504+
directly. If an unexpected error is thrown by user code rather than a handled error,
505+
that error will be transformed into a `SerializedError` shape. Users should make sure that they are checking which kind of error they are dealing with before attempting to access its properties. This can be done in a type safe manner either
506+
by using a type guard, e.g. by checking for [discriminated properties](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-in-operator-narrowing),
507+
or using a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates).
508+
509+
When using [`fetchBaseQuery`](./api/fetchBaseQuery.mdx), as your base query,
510+
errors will be of type `FetchBaseQueryError | SerializedError`. The specific shapes of those types can be seen below.
511+
512+
```ts title="FetchBaseQueryError type"
513+
export type FetchBaseQueryError =
514+
| {
515+
/**
516+
* * `number`:
517+
* HTTP status code
518+
*/
519+
status: number
520+
data: unknown
521+
}
522+
| {
523+
/**
524+
* * `"FETCH_ERROR"`:
525+
* An error that occurred during execution of `fetch` or the `fetchFn` callback option
526+
**/
527+
status: 'FETCH_ERROR'
528+
data?: undefined
529+
error: string
530+
}
531+
| {
532+
/**
533+
* * `"PARSING_ERROR"`:
534+
* An error happened during parsing.
535+
* Most likely a non-JSON-response was returned with the default `responseHandler` "JSON",
536+
* or an error occurred while executing a custom `responseHandler`.
537+
**/
538+
status: 'PARSING_ERROR'
539+
originalStatus: number
540+
data: string
541+
error: string
542+
}
543+
| {
544+
/**
545+
* * `"CUSTOM_ERROR"`:
546+
* A custom error type that you can return from your `queryFn` where another error might not make sense.
547+
**/
548+
status: 'CUSTOM_ERROR'
549+
data?: unknown
550+
error: string
551+
}
552+
```
553+
554+
```ts title="SerializedError type"
555+
export interface SerializedError {
556+
name?: string
557+
message?: string
558+
stack?: string
559+
code?: string
560+
}
561+
```
562+
563+
### Error result example
564+
565+
When using `fetchBaseQuery`, the `error` property returned from a hook will have the type `FetchBaseQueryError | SerializedError | undefined`.
566+
If an error is present, you can access error properties after narrowing the type to either `FetchBaseQueryError` or `SerializedError`.
567+
568+
```tsx
569+
import { api } from './services/api'
570+
571+
function PostDetail() {
572+
const { data, error, isLoading } = usePostsQuery()
573+
574+
if (isLoading) {
575+
return <div>Loading...</div>
576+
}
577+
578+
if (error) {
579+
if ('status' in error) {
580+
// you can access all properties of `FetchBaseQueryError` here
581+
const errMsg = 'error' in err ? err.error : JSON.stringify(err.data)
582+
583+
return (
584+
<div>
585+
<div>An error has occurred:</div>
586+
<div>{errMsg}</div>
587+
</div>
588+
)
589+
}
590+
else {
591+
// you can access all properties of `SerializedError` here
592+
return <div>{error.message}</div>
593+
}
594+
}
595+
596+
if (data) {
597+
return (
598+
<div>
599+
{data.map((post) => (
600+
<div key={post.id}>Name: {post.name}</div>
601+
))}
602+
</div>
603+
)
604+
}
605+
606+
return null
607+
}
608+
```
609+
610+
### Inline error handling example
611+
612+
When handling errors inline after [`unwrapping`](../api/createAsyncThunk.mdx#unwrapping-result-actions) a mutation call,
613+
a thrown error will have a type of `any` for typescript versions below 4.4,
614+
or [`unknown` for versions 4.4+](https://devblogs.microsoft.com/typescript/announcing-typescript-4-4/#use-unknown-catch-variables).
615+
In order to safely access properties of the error, you must first narrow the type to a known type.
616+
This can be done using a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates)
617+
as shown below.
618+
619+
```tsx title="services/helpers.ts"
620+
import { FetchBaseQueryError } from '@reduxjs/toolkit/query'
621+
622+
/**
623+
* Type predicate to narrow an unknown error to `FetchBaseQueryError`
624+
*/
625+
export function isFetchBaseQueryError(
626+
error: unknown
627+
): error is FetchBaseQueryError {
628+
return typeof error === 'object' && error != null && 'status' in error
629+
}
630+
631+
/**
632+
* Type predicate to narrow an unknown error to an object with a string 'message' property
633+
*/
634+
export function isErrorWithMessage(
635+
error: unknown
636+
): error is { message: string } {
637+
return (
638+
typeof error === 'object' &&
639+
error != null &&
640+
'message' in error &&
641+
typeof (error as any).message === 'string'
642+
)
643+
}
644+
```
645+
646+
```tsx title="addPost.tsx"
647+
import { useState } from 'react'
648+
import { useSnackbar } from 'notistack'
649+
import { api } from './services/api'
650+
import { isFetchBaseQueryError, isErrorWithMessage } from './services/helpers'
651+
652+
function AddPost() {
653+
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
654+
const [name, setName] = useState('')
655+
const [addPost] = useAddPostMutation()
656+
657+
async function handleAddPost() {
658+
try {
659+
await addPost(name).unwrap()
660+
setName('')
661+
} catch (err) {
662+
if (isFetchBaseQueryError(err)) {
663+
// you can access all properties of `FetchBaseQueryError` here
664+
const errMsg = 'error' in err ? err.error : JSON.stringify(err.data)
665+
enqueueSnackbar(errMsg, { variant: 'error' })
666+
} else if (isErrorWithMessage(err)) {
667+
// you can access a string 'message' property here
668+
enqueueSnackbar(err.message, { variant: 'error' })
669+
}
670+
}
671+
}
672+
673+
return (
674+
<div>
675+
<input value={name} onChange={(e) => setName(e.target.value)} />
676+
<button>Add post</button>
677+
</div>
678+
)
679+
}
680+
```

packages/toolkit/src/query/fetchBaseQuery.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export type FetchBaseQueryError =
7070
| {
7171
/**
7272
* * `"FETCH_ERROR"`:
73-
* An error that occured during execution of `fetch` or the `fetchFn` callback option
73+
* An error that occurred during execution of `fetch` or the `fetchFn` callback option
7474
**/
7575
status: 'FETCH_ERROR'
7676
data?: undefined
@@ -81,7 +81,7 @@ export type FetchBaseQueryError =
8181
* * `"PARSING_ERROR"`:
8282
* An error happened during parsing.
8383
* Most likely a non-JSON-response was returned with the default `responseHandler` "JSON",
84-
* or an error occured while executing a custom `responseHandler`.
84+
* or an error occurred while executing a custom `responseHandler`.
8585
**/
8686
status: 'PARSING_ERROR'
8787
originalStatus: number
@@ -91,7 +91,7 @@ export type FetchBaseQueryError =
9191
| {
9292
/**
9393
* * `"CUSTOM_ERROR"`:
94-
* A custom error type that you can return from your `fetchFn` where another error might not make sense.
94+
* A custom error type that you can return from your `queryFn` where another error might not make sense.
9595
**/
9696
status: 'CUSTOM_ERROR'
9797
data?: unknown

0 commit comments

Comments
 (0)