| name | TanStack Errors |
| description | This skill should be used when the user asks about "React Query error handling", "error boundaries with useQuery", "throwOnError", "global error callbacks", "onError callback", "query error state", "mutation error handling", or needs guidance on error handling strategies and patterns in TanStack Query. |
| version | 1.0.0 |
TanStack Query Error Handling Patterns
This skill provides guidance for error handling in TanStack Query, covering local vs global strategies, Error Boundaries, and best practices based on TKDodo's recommendations.
Three Approaches to Error Handling
1. Local Error State
Check the error property directly in components:
function TodoList() {
const { data, error, isError } = useQuery(todoQueries.all())
if (isError) {
return <ErrorMessage error={error} />
}
return <ul>{data?.map(todo => <TodoItem key={todo.id} todo={todo} />)}</ul>
}
Pros: Simple, explicit, component controls its error UI
Cons: Repetitive across many components
2. Error Boundaries
Propagate errors to Error Boundaries using throwOnError:
function TodoList() {
const { data } = useQuery({
...todoQueries.all(),
throwOnError: true,
})
return <ul>{data.map(todo => <TodoItem key={todo.id} todo={todo} />)}</ul>
}
<ErrorBoundary fallback={<ErrorPage />}>
<TodoList />
</ErrorBoundary>
3. Global Error Callbacks
Handle errors at the QueryCache level:
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
if (query.state.data !== undefined) {
toast.error(`Background update failed: ${error.message}`)
}
},
}),
})
Choosing the Right Approach
| Scenario | Recommended Approach |
|---|
| Critical page data | Error Boundary |
| Background refetch failures | Global callbacks with toast |
| Form validation errors | Local error state |
| 404 errors | Local error state or boundary |
| 5xx server errors | Error Boundary |
| Network errors | Global callbacks |
Error Boundaries with throwOnError
Basic Usage
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
throwOnError: true,
})
Conditional Throwing
Only throw for specific error types:
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
throwOnError: (error) => {
return error.response?.status >= 500
},
})
This pattern allows:
- 5xx errors → Error Boundary (page-level error)
- 4xx errors → Local handling (validation, not found)
Creating an Error Boundary
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)}
>
<Routes />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
)
}
QueryErrorResetBoundary ensures queries retry when the boundary resets.
Global Error Callbacks
The Problem with Per-Query onError
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
onError: (error) => {
toast.error(error.message)
},
})
If three components use this query, you get three toast notifications.
The Solution: QueryCache Callbacks
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
toast.error(`Error: ${error.message}`)
},
}),
})
Smarter Global Handling
Only show errors when they matter:
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
if (query.state.data !== undefined) {
toast.error(`Failed to refresh: ${error.message}`)
}
},
}),
mutationCache: new MutationCache({
onError: (error) => {
toast.error(`Operation failed: ${error.message}`)
},
}),
})
Prerequisites: Making Errors Happen
Fetch API Doesn't Throw on HTTP Errors
const fetchTodo = async (id: string) => {
const response = await fetch(`/api/todos/${id}`)
return response.json()
}
const fetchTodo = async (id: string) => {
const response = await fetch(`/api/todos/${id}`)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response.json()
}
Axios Throws by Default
const fetchTodo = async (id: string) => {
const { data } = await axios.get(`/api/todos/${id}`)
return data
}
Don't Swallow Errors
const fetchTodo = async (id: string) => {
try {
const response = await api.get(`/todos/${id}`)
return response.data
} catch (error) {
console.error(error)
}
}
const fetchTodo = async (id: string) => {
try {
const response = await api.get(`/todos/${id}`)
return response.data
} catch (error) {
console.error(error)
throw error
}
}
Mutation Error Handling
Local Handling with mutate Callbacks
const mutation = useMutation({
mutationFn: createTodo,
})
mutation.mutate(newTodo, {
onError: (error) => {
setFormError(error.message)
},
})
Global Mutation Errors
const queryClient = new QueryClient({
mutationCache: new MutationCache({
onError: (error, variables, context, mutation) => {
logger.error('Mutation failed', { error, variables })
if (!mutation.meta?.skipToast) {
toast.error('Operation failed. Please try again.')
}
},
}),
})
useMutation({
mutationFn: saveForm,
meta: { skipToast: true },
})
Pattern: Combining Approaches
Use multiple strategies together:
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
logger.error('Query failed', { error, queryKey: query.queryKey })
if (query.state.data !== undefined) {
toast.error('Failed to refresh data')
}
},
}),
})
function TodoPage() {
return (
<ErrorBoundary fallback={<TodoPageError />}>
<TodoList />
</ErrorBoundary>
)
}
function TodoList() {
const { data, error, isError } = useQuery({
...todoQueries.all(),
throwOnError: (error) => error.status >= 500,
})
if (isError && error.status === 404) {
return <EmptyState message="No todos found" />
}
return <ul>{data?.map(...)}</ul>
}
Quick Reference
| Error Type | Strategy |
|---|
| Network failure | Global toast + retry |
| 5xx server error | Error Boundary |
| 404 Not Found | Local state |
| 400 Validation | Local state (show form errors) |
| 401 Unauthorized | Global redirect to login |
| Background refetch fail | Global toast only |
| Mutation fail | Local state or global toast |
Additional Resources
Reference Files
For detailed patterns and advanced techniques, consult:
references/retry-patterns.md - Retry strategies and configuration
Related Skills
- tanstack-query - Core concepts, staleTime
- tanstack-mutations - Mutation-specific error handling
- tanstack-types - Typed error handling