TanStack Query Error Handling in Production: Guide 2026
React Query retries silently and swallows failures. Set up global error handling, retry rules, error boundaries, and logging so API failures never go unnoticed.
TanStack Query (React Query) retries failed requests three times by default and keeps showing stale data, so API failures can go unseen; handle errors in production with a global QueryCache handler for logging, targeted retry rules, and error boundaries for rendering.
Left on defaults, a broken endpoint looks like a slow page: the query retries with exponential backoff, then shows a loading spinner or old data. Nothing is reported unless you wire it up.
Step 1: Log Every Failure Globally
Since v5, the onError, onSuccess, and onSettled callbacks were removed from useQuery. Handle errors once at the cache level instead:
import { QueryClient, QueryCache, MutationCache } from '@tanstack/react-query'
export const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
reportError(error, { queryKey: query.queryKey })
},
}),
mutationCache: new MutationCache({
onError: (error) => reportError(error, { type: 'mutation' }),
}),
})
One handler means no failure is missed because a developer forgot to add onError on one hook. Include the query key so you can tell which call broke.
Step 2: Do Not Retry Errors That Cannot Succeed
Retrying a 404 or 401 three times wastes seconds and hides the real problem.
new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error: any) => {
const status = error?.status
if (status && status >= 400 && status < 500) return false // client errors
return failureCount < 2
},
staleTime: 30_000,
},
},
})
Throw a typed error from your fetcher so status is available:
async function api<T>(url: string): Promise<T> {
const res = await fetch(url)
if (!res.ok) throw Object.assign(new Error(`HTTP ${res.status}`), { status: res.status })
return res.json()
}
fetch does not reject on HTTP errors. If you skip this check, a 500 response resolves as "success" and the failure never reaches onError. Validate the response shape too, using Zod.
Step 3: Render Errors With Boundaries
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => (
<button onClick={resetErrorBoundary}>Something failed. Retry</button>
)}>
<Orders />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
Pair it with useQuery({ throwOnError: true }) or useSuspenseQuery, so failures render the fallback instead of an empty screen. More patterns in React Error Boundaries in Production.
Step 4: Treat Mutations Differently
Mutations do not retry by default, which is correct: retrying a payment can double-charge. Surface the error to the user and keep the form data.
| Query type | Retry | User feedback |
|---|---|---|
| Read (GET) | Yes, limited | Fallback UI or stale data with a banner |
| Write (POST) | No, unless idempotent | Inline error, keep input |
| Background refetch | Yes | Quiet, log only |
Common Production Gotchas
- Stale data hides outages. Show a "could not refresh" indicator when
isErroris true anddatastill exists. - Refetch on window focus can hammer a failing API. Disable it or add backoff during incidents.
- Server vs client errors. Failures in Next.js server prefetching differ from client ones; see Next.js API route error handling.
- 429 responses need longer backoff; see OpenAI API 429 handling for the pattern.
Let Relia Watch the API Calls Behind the UI
Relia records the user's session, the failing request, and the backend error together. When a query starts failing for real users, it writes the incident up in plain words: what happened, why it thinks so (with a confidence level), and one thing to do next. On request it writes a fix plan with a "Copy for your AI assistant" button; Relia doesn't change code itself, and after you ship it checks the fix against live traffic.
FAQ
Why was onError removed from useQuery in v5?
Per-hook callbacks fired once per observer and led to duplicate or missed handling. Global cache callbacks fire once per query.
How many times does React Query retry by default?
Three times with exponential backoff. Override retry per query or globally.
Does React Query catch fetch HTTP errors?
Only if your query function throws. Check res.ok and throw.