Implementing a Global Error Handler with React Query
There is a nice trick within the amazing React Query library that is not well-known which enables us to catch all errors that will be thrown by mutations or queries inside the whole project. The trick is to define our own MutationCache
or QueryCache
and pass it to our QueryClient
instance.
function MyApp({ Component, pageProps }: AppProps) {
const [queryClient] = useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Component {...pageProps} />
</Hydrate>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
This is a typical React Query setup. Here React Query will implicitly create for us our MutationCache
and QueryCache
that will be used by the QueryClient
instance. However we can define our own instances of the caches and pass it to the constructor of QueryClient
.
For the rest of the guide I will only implement the MutationCache
but the exact same steps could be taken for the QueryCache
.
function MyApp({ Component, pageProps }: AppProps) {
const mutationCache = new MutationCache()
const [queryClient] = useState(() => new QueryClient({ mutationCache }))
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Component {...pageProps} />
</Hydrate>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
The MutationCache
constructor takes a lot of options, the most relevant one for now is to define a typical onError
function, like the normal ones you'd implement with useMutation
or useQuery
.
function MyApp({ Component, pageProps }: AppProps) {
const mutationCache = new MutationCache({
onError: (error) => {
// any error handling code...
console.error(error)
},
})
const [queryClient] = useState(() => new QueryClient({ mutationCache }))
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Component {...pageProps} />
</Hydrate>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
With those simple few lines we already implemented our global error handler. Now any mutation fired with useMutation
thought the whole project will pass by this onError
if it results in an error. You even have access to the error
object so you can add conditional logic, like: "only displaying a toast if the status is >=500
".
One small issue remains... What if you want to handle an error using a different way in a specific useMutation
instance, i.e. you want to define a specific onError
for that mutation but obviously you want all other mutations to pass by the global onError
we defined above. With the current implementation, it will pass by both, which is obviously an undesirable behavior.
Making matters worse, it wall pass by the global one first, which prevents us from passing any data through the error
object that we can then use inside the onError
of the MutationCache
.
Don't worry! There is an elegant solution for this problem. The onError
of the MutationCache
also receives a mutation
object which contains the relevant options used in the useMutation
call. This is perfect because this is exposes wether the useMutation
that fired this api call had an onError
defined or not. We can then conditionally handle the error or ignore it.
function MyApp({ Component, pageProps }: AppProps) {
const mutationCache = new MutationCache({
onError: (error, _variables, _context, mutation) => {
// If this mutation has an onError defined, skip this
if (mutation.options.onError) return
// any error handling code...
console.error(error)
},
})
const [queryClient] = useState(() => new QueryClient({ mutationCache }))
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Component {...pageProps} />
</Hydrate>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}