@tanstack/react-query
semver
>=5.0.0 <6.0.0postconditions30functions16last verified2026-06-24coverage score100%Postconditions: what we check
- useQuery · query-error-unhandledwarningWhenqueryFn throws an error and error state is not checkedThrows
Error (type specified by TError generic, defaults to Error)Required handlingCaller SHOULD handle query errors using one of these methods: 1. Check the `error` property and `isError` state returned by useQuery, OR 2. Wrap the component in an ErrorBoundary with throwOnError: true, OR 3. Configure global QueryCache.onError handler in QueryClient (React Query v5 recommended pattern). Query errors are NOT thrown - they are returned in the error property. Ignoring the error state leaves users with broken UI and no feedback. Global QueryCache error handlers (configured in QueryClient) satisfy this requirement.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - useQuery · query-retry-client-errorswarningWhenretry is configured without checking error type (retries on 4xx)Throws
N/ARequired handlingWhen configuring custom retry logic, MUST NOT retry on client errors (400, 401, 403, 404). Bad: retry: 3 (retries all errors including 4xx) Good: retry: (failureCount, error) => { if (error.response?.status >= 400 && error.response?.status < 500) return false; return failureCount < 3; } Retrying client errors wastes resources and delays user feedback. Only server errors (5xx) and network errors should trigger retries.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[3] - useQuery · stale-query-refetch-errorerrorWhenstale query refetches in background and new fetch failsThrows
Error from queryFnRequired handlingWhen staleTime expires and background refetch fails, the error property will be populated. Previous cached data remains available via the `data` property. Caller MUST either: 1. Check `isError` state and show error UI/toast, OR 2. Configure global error handler via QueryCache onError, OR 3. Use `failureReason` to track errors without entering error state. Silent failures leave users viewing stale data with no indication of problems.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[4] - useQuery · network-error-handlingerrorWhennetwork failure prevents query from executingThrows
Network error (TypeError, fetch errors, etc.)Required handlingNetwork errors (DNS failures, timeouts, connection refused) are treated as query errors. Caller MUST handle network errors separately from HTTP errors: 1. Check error type/message to distinguish network vs HTTP errors 2. Show appropriate user feedback ("No internet connection" vs "Server error") 3. Consider different retry strategies (network errors may need exponential backoff) Treating all errors the same provides poor user experience.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[1] - useMutation · mutation-error-unhandledwarningWhenmutationFn throws an error and error is not handledThrows
Error (type specified by TError generic, defaults to Error)Required handlingCaller SHOULD handle mutation errors using one of these methods: 1. Check the `error` property and `isError` state returned by useMutation, OR 2. Provide onError callback in mutation options, OR 3. Handle errors in the .catch() when calling mutate/mutateAsync, OR 4. Configure global mutation error handler via QueryClient mutationCache.onError (v5 recommended pattern). Mutations modify server state - users MUST know if they failed. Silent failures lead to data inconsistency and user confusion. Global MutationCache error handlers satisfy this requirement.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[5] - useMutation · mutation-optimistic-update-rollbackerrorWhenoptimistic update is performed and mutation failsThrows
Error from mutationFnRequired handlingWhen using optimistic updates, the onError callback MUST roll back the optimistic changes. Required pattern: ``` onMutate: async (newData) => { await queryClient.cancelQueries({ queryKey }) const previousData = queryClient.getQueryData(queryKey) queryClient.setQueryData(queryKey, newData) // optimistic update return { previousData } // context for rollback }, onError: (err, newData, context) => { queryClient.setQueryData(queryKey, context.previousData) // ROLLBACK } ``` Without rollback, failed mutations leave UI showing incorrect data.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[6] - useMutation · mutation-default-no-retrywarningWhenmutation fails and caller expects retry behaviorThrows
N/ARequired handlingMutations do NOT retry by default (unlike queries). If retry is needed for idempotent mutations: 1. Explicitly configure retry option 2. Ensure mutation is idempotent or uses idempotency keys 3. Only retry on network/server errors, NOT client errors Bad: retry: 3 (may duplicate non-idempotent operations) Good: retry: false (default, explicit) Acceptable: retry: (failureCount, error) => { // Only retry GET-like mutations or with idempotency keys return error.response?.status >= 500 && failureCount < 2 }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[5] - useMutation · mutation-parallel-executionwarningWhenmultiple mutations executing concurrently cause race conditionsThrows
N/ARequired handlingBy default, mutations execute in parallel which can cause race conditions. For sequential mutations, caller MUST: 1. Use mutateAsync with await for sequential execution, OR 2. Use onSuccess callback to chain mutations, OR 3. Configure mutation queue in QueryClient. Parallel mutations can cause: last-write-wins conflicts, data inconsistency, incorrect UI state.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[5] - useInfiniteQuery · infinite-query-error-unhandledwarningWhenfetchNextPage fails and error is not handledThrows
Error from queryFn for failed pageRequired handlingCaller SHOULD handle errors when fetching additional pages: 1. Check `error` and `isError` state, OR 2. Check `fetchNextPageError` for page-specific errors, OR 3. Configure global QueryCache.onError handler (v5 recommended pattern). When fetchNextPage fails, previously loaded pages remain accessible via `data`. User must be informed that loading more failed. Global QueryCache error handlers satisfy this requirement.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[7] - useInfiniteQuery · infinite-query-refetch-all-pageserrorWhenquery is refetched and middle pages failThrows
Error from queryFnRequired handlingWhen refetching infinite query, ALL pages are refetched by default. If any page fails, the entire query enters error state. Caller MUST either: 1. Handle errors gracefully (show toast, retry button), OR 2. Configure refetchPages to only refetch first page: refetchPages: 'first', OR 3. Use refetchOnWindowFocus: false to prevent automatic refetches. Refetch failures can lose all previously loaded pages if not handled.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[7] - useSuspenseQuery · suspense-query-error-boundary-requirederrorWhenqueryFn throws any errorThrows
Error thrown by queryFn — propagated to nearest ErrorBoundary (not returned in result)Required handlingUnlike useQuery, useSuspenseQuery does NOT return errors in the result object. All errors are thrown to the nearest ErrorBoundary. Caller MUST: 1. Wrap the component tree in a React ErrorBoundary to catch thrown errors, AND 2. Wrap with QueryErrorResetBoundary (or use useQueryErrorResetBoundary) to allow retry. Required pattern: ```tsx <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallback={<ErrorUI />}> <ComponentUsingSuspenseQuery /> </ErrorBoundary> )} </QueryErrorResetBoundary> ``` Without ErrorBoundary, errors propagate unhandled and crash the component tree. Without QueryErrorResetBoundary, users cannot retry failed queries.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - useSuspenseQuery · suspense-query-stale-cache-hides-errorswarningWhenbackground refetch fails but stale cached data existsThrows
Error is NOT thrown to ErrorBoundary when cached data exists (even if stale)Required handlingWhen cached data exists and a background refetch fails, React Query does NOT throw the error to the ErrorBoundary — it continues showing stale data silently. To catch all errors regardless of cache state: ```tsx const { data, error, isFetching } = useSuspenseQuery(...) if (error && !isFetching) { throw error // Manually propagate to ErrorBoundary } ``` Or use throwOnError: true in query options to always throw. Silent background failures leave users viewing stale data with no indication.costmediumin proddegraded serviceusers seedegraded performancevisibilitysilentSources[8] - useSuspenseQuery · suspense-cancellation-not-supportedwarningWhencomponent unmounts while query is in-flightThrows
CancelledError may not propagate as expected — cancellation does not work with Suspense hooksRequired handlingCancellation via AbortSignal does not function correctly with useSuspenseQuery. Do not rely on query cancellation patterns when using Suspense variants. If cancellation on unmount is required, use standard useQuery instead.costlowin proddegraded serviceusers seedegraded performancevisibilitysilentSources[9] - useSuspenseInfiniteQuery · suspense-infinite-error-boundary-requirederrorWhenany page fetch throws an errorThrows
Error thrown by queryFn — propagated to nearest ErrorBoundaryRequired handlingAll errors from page fetches are thrown to the nearest ErrorBoundary. throwOnError cannot be configured (always true). Caller MUST wrap in QueryErrorResetBoundary + ErrorBoundary: ```tsx <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallback={<ErrorUI />}> <InfiniteList /> </ErrorBoundary> )} </QueryErrorResetBoundary> ``` Without this, any pagination error crashes the entire component tree.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - useQueries · parallel-query-partial-failurewarningWhenone or more queries in the array fail while others succeedThrows
Individual errors are returned in each result's error property (not thrown)Required handlingWhen some queries fail and others succeed, the results array contains mixed states. Errors are NOT thrown — they are in result[i].error for each failed query. Caller MUST check each result independently: ```tsx const results = useQueries({ queries: [...] }) const errors = results.filter(r => r.isError).map(r => r.error) const allLoaded = results.every(r => !r.isPending) ``` Ignoring per-query error states means some data silently fails while UI renders partial results — a hard-to-debug data consistency problem.costmediumin proddegraded serviceusers seedegraded performancevisibilitysilentSources[11] - useQueries · combine-loses-error-infoerrorWhencombine option is used and error properties are not explicitly forwardedThrows
N/A — errors become inaccessible if combine does not include error propertiesRequired handlingThe combine function transforms results into a single value. If error properties are not explicitly included in the combine output, they are lost. Bad (errors lost): ```tsx combine: (results) => results.map(r => r.data) // error info discarded ``` Good (errors preserved): ```tsx combine: (results) => ({ data: results.map(r => r.data), errors: results.filter(r => r.isError).map(r => r.error), hasError: results.some(r => r.isError), }) ``` Dropped errors create invisible failures — queries fail silently, users see incomplete data with no error indication.costmediumin prodsilent failureusers seelost datavisibilitysilentSources[11] - fetchQuery · fetchquery-throws-on-errorerrorWhenqueryFn throws an error (network failure, HTTP error, etc.)Throws
Error from queryFn — thrown directly (not returned in result object)Required handlingfetchQuery THROWS on error. Unlike useQuery, there is no .error property. Caller MUST wrap in try-catch: ```typescript try { const data = await queryClient.fetchQuery({ queryKey, queryFn }) return { props: { data } } } catch (error) { // Handle appropriately for context: // SSR: return { notFound: true } or { redirect: { destination: '/error' } } // Route loader: throw new Response('Not Found', { status: 404 }) // Component: propagate or show error state } ``` In SSR contexts (Next.js getServerSideProps), uncaught fetchQuery errors cause 500 responses and crash the server-side render. Users see blank pages or generic error pages.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - fetchQuery · fetchquery-ssr-uncaught-errorerrorWhenfetchQuery called during SSR without try-catchThrows
Error propagates to SSR framework — causes 500 response or build failureRequired handlingIn Next.js App Router (RSC), Remix loaders, or similar SSR contexts, an uncaught fetchQuery error causes the entire page render to fail. Pattern for critical SSR data: ```typescript // Next.js getServerSideProps export async function getServerSideProps(context) { try { await queryClient.fetchQuery({ queryKey: ['user', id], queryFn }) } catch (e) { return { redirect: { destination: '/404', permanent: false } } } return { props: { dehydratedState: dehydrate(queryClient) } } } ``` For non-critical data, use prefetchQuery instead (never throws).costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - fetchInfiniteQuery · fetchinfinitequery-throws-on-errorerrorWhenany page fetch failsThrows
Error from queryFn for the failing pageRequired handlingfetchInfiniteQuery THROWS on error. Must be wrapped in try-catch. ```typescript try { await queryClient.fetchInfiniteQuery({ queryKey: ['posts'], queryFn: fetchPosts, initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextCursor, }) } catch (error) { // Handle SSR failure — fallback or error response } ``` In SSR contexts, uncaught errors cause 500 responses. For non-critical prefetching, use prefetchInfiniteQuery instead.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[12] - ensureQueryData · ensurequerydata-throws-when-fetch-needederrorWhencache miss or stale data triggers a fetch that failsThrows
Error from queryFn — thrown when underlying fetch is needed and failsRequired handlingensureQueryData fetches if no fresh cache exists. When the fetch fails, it throws. ```typescript try { const data = await queryClient.ensureQueryData({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), }) } catch (error) { // Handle: redirect to error page, return fallback, etc. } ``` Called in Next.js Route Handlers or RSC without try-catch, a fetch failure causes an unhandled rejection that crashes the request handler.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[12] - invalidateQueries · invalidatequeries-silent-refetch-failureinfoWhenrefetch triggered by invalidation fails and throwOnError is not setThrows
No error thrown by default (throwOnError defaults to false)Required handlingBy default, invalidateQueries does NOT throw if refetch tasks fail. Failed refetches populate the individual query's error state silently. This is expected React Query behavior — the background refetch is fire-and-forget by design. No action is required in the vast majority of use cases. If explicit post-mutation error handling is needed, pass throwOnError: true: ```typescript try { await queryClient.invalidateQueries({ queryKey: ['todos'], throwOnError: true, // throws if any refetch fails }) } catch (error) { toast.error('Failed to refresh data') } ``` Without throwOnError, silent refetch failures leave UI showing stale data after a mutation with no user notification — but this is the documented default.costlowin proddegraded serviceusers seedegraded performancevisibilitysilentSources[12] - refetchQueries · refetchqueries-silent-failurewarningWhenrefetch fails and throwOnError is not setThrows
No error thrown by default (throwOnError defaults to false)Required handlingrefetchQueries does NOT throw on failure by default. Errors go into the individual query's error state, not the refetchQueries Promise. To catch refetch failures explicitly: ```typescript try { await queryClient.refetchQueries({ queryKey: ['dashboard'], throwOnError: true, }) } catch (error) { toast.error('Failed to refresh dashboard') } ``` Common in "pull-to-refresh" patterns where users expect visible feedback on failure. Without throwOnError, a failed manual refresh is invisible to the caller.costlowin proddegraded serviceusers seedegraded performancevisibilitysilentSources[12] - prefetchQuery · prefetchquery-silently-swallows-errorswarningWhenqueryFn throws an error during prefetchThrows
No error thrown — error is silently discardedRequired handlingprefetchQuery intentionally swallows all errors. This is by design. The query will be retried by useQuery when the component mounts on the client. If you NEED to handle prefetch failures (e.g., for critical SSR content where a missing resource should return 404), use fetchQuery instead: ```typescript // Use fetchQuery when failure requires an error response: try { await queryClient.fetchQuery({ queryKey, queryFn }) } catch (error) { return { notFound: true } } // Use prefetchQuery when graceful degradation is acceptable: await queryClient.prefetchQuery({ queryKey, queryFn }) // never throws ``` Misusing prefetchQuery for critical data means SSR silently serves incomplete pages while clients retry, causing flash of missing content.costlowin proddegraded serviceusers seedegraded performancevisibilityvisibleSources[14] - ensureInfiniteQueryData · ensureinfinitequerydata-throws-when-fetch-needederrorWhencache miss or stale data triggers a fetchInfiniteQuery call that failsThrows
Error from queryFn for the failing page — thrown directly (not returned in result)Required handlingensureInfiniteQueryData fetches if no fresh infinite query cache exists. When the underlying fetchInfiniteQuery fails, the error is thrown to the caller. Caller MUST wrap in try-catch when used in SSR or route loaders: ```typescript try { const data = await queryClient.ensureInfiniteQueryData({ queryKey: ['posts'], queryFn: ({ pageParam = 0 }) => fetchPosts(pageParam), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextCursor, }) } catch (error) { // Handle: redirect to error page, return fallback, return 404 return { notFound: true } } ``` In Next.js App Router (RSC) or Remix loaders, an uncaught error causes the entire page render to fail with a 500 response. For non-critical prefetching where graceful degradation is acceptable, use prefetchInfiniteQuery instead (never throws).costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - ensureInfiniteQueryData · ensureinfinitequerydata-revalidate-if-stale-silent-background-errorwarningWhenrevalidateIfStale: true is set, cache is stale, and the background refetch failsThrows
No error thrown — stale cached data is returned immediately, background refetch error is silently discardedRequired handlingWhen revalidateIfStale: true is configured and the cache is stale: 1. Stale data is returned immediately (the call resolves with stale data), AND 2. A background refetch is triggered but any failure is silently swallowed. The background error will appear in the individual query's error state (accessible via useInfiniteQuery error property) but NOT propagated to the caller. ```typescript // This resolves with stale data — background error is NOT thrown const data = await queryClient.ensureInfiniteQueryData({ queryKey: ['posts'], queryFn: fetchPosts, initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextCursor, revalidateIfStale: true, // background refetch; error silently discarded }) // data may be stale if background refetch failed ``` To detect background refresh failures, monitor the query error state via useInfiniteQuery or configure a global QueryCache.onError handler.costmediumin proddegraded serviceusers seedegraded performancevisibilitysilentSources[15] - useSuspenseQueries · suspense-queries-error-boundary-requirederrorWhenany query in the array throws an error during fetchThrows
Error thrown by the failing queryFn — propagated to nearest ErrorBoundary (not returned in result)Required handlingUnlike useQueries, useSuspenseQueries does NOT return errors in each result's error property. Any single query failure throws to the nearest ErrorBoundary — the entire component subtree unmounts and the fallback renders. throwOnError CANNOT be configured (excluded from the options type). You cannot opt out of throw-on-error per-query; the whole array is all-or-nothing. Caller MUST: 1. Wrap the consuming component in a React ErrorBoundary to catch thrown errors, AND 2. Wrap with QueryErrorResetBoundary (or use useQueryErrorResetBoundary) to allow retry. Required pattern: ```tsx <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallback={<ErrorUI />}> <ComponentUsingSuspenseQueries /> </ErrorBoundary> )} </QueryErrorResetBoundary> ``` Without ErrorBoundary, ANY failing query crashes the component tree — even if the other queries succeeded. There is no partial-success result shape with useSuspenseQueries (unlike useQueries where mixed states are returned).costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - useSuspenseQueries · suspense-queries-cancellation-not-supportedwarningWhencomponent unmounts while one or more queries are in-flightThrows
CancelledError does not propagate as expected — cancellation does not work with Suspense hooksRequired handlingCancellation via AbortSignal does not function correctly with useSuspenseQueries. In-flight queries continue running after the component unmounts. Do not rely on query cancellation patterns when using Suspense variants. If cancellation on unmount is required (e.g., for expensive long-running fetches), use standard useQueries instead and check per-result error/loading states.costlowin proddegraded serviceusers seedegraded performancevisibilitysilentSources[17] - useSuspenseQueries · suspense-queries-stale-refetch-cascadewarningWhenone query in the array takes significantly longer than others to load on initial mountThrows
N/A — silent re-fetch cascade on re-mountRequired handlingThe component does not re-mount until ALL queries have finished loading. During that window, any query that completed quickly may go stale (staleTime defaults to 0). On re-mount, every stale query refetches automatically — causing a cascade of duplicate requests immediately after Suspense resolves. For arrays containing one slow query and several fast ones, set staleTime explicitly: ```tsx useSuspenseQueries({ queries: [ { queryKey: ['fast-1'], queryFn: fetchFast1, staleTime: 60_000 }, { queryKey: ['slow'], queryFn: fetchSlow, }, { queryKey: ['fast-2'], queryFn: fetchFast2, staleTime: 60_000 }, ], }) ``` Without staleTime, the network traffic doubles on first mount, which causes rate-limit pressure on the slowest endpoint and adds latency to interactive use.costlowin proddegraded serviceusers seedegraded performancevisibilitysilentSources[17] - resetQueries · resetqueries-silent-refetch-failureinfoWhenrefetch triggered by reset fails and throwOnError is not setThrows
No error thrown by default (throwOnError defaults to undefined / false)Required handlingBy default, resetQueries does NOT throw if the post-reset refetch fails. The reset itself (clearing query state) always succeeds; only the subsequent refetch of active queries can fail, and that failure is silently swallowed unless throwOnError: true is passed. For most use cases this is correct behavior — resetting on logout/account-switch should not error even if a background refetch happens to fail. This postcondition documents that fact so suppress-or-fix decisions are auditable. If post-reset refetch failure must be surfaced (e.g., for test assertions or explicit "reset and reload" UI), pass throwOnError: true: ```typescript try { await queryClient.resetQueries({ queryKey: ['user-data'], throwOnError: true, }) } catch (error) { toast.error('Failed to reload after reset') } ```costlowin proddegraded serviceusers seedegraded performancevisibilitysilentSources[19] - resetQueries · resetqueries-throws-when-throw-on-errorwarningWhenoptions.throwOnError is true and any reset-triggered refetch failsThrows
Error from the failing queryFn — thrown directly (Promise.all rejects with first failure)Required handlingWhen throwOnError: true is passed, resetQueries throws on any refetch failure. Caller MUST wrap in try-catch: ```typescript try { await queryClient.resetQueries({ queryKey: ['session'], throwOnError: true, }) } catch (error) { // Handle: log, toast, fallback navigation, etc. console.error('reset-and-refetch failed:', error) } ``` Common in test setup/teardown where assertion of clean state is required, or in critical-path flows (e.g., account switch) where stale data after reset would be a security/correctness issue.costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[19]
Sources
Every postcondition cites at least one of these. Grouped by source type; numbered to match the footnotes above.
Official documentation
- [1]tanstack.com/query/latest/docsQuery Functions
- [3]tanstack.com/query/latest/docsQuery Retries
- [4]tanstack.com/query/latest/docsCaching
- [5]tanstack.com/query/latest/docsMutations
- [6]tanstack.com/query/latest/docsOptimistic Updates
- [7]tanstack.com/query/latest/docsInfinite Queries
- [8]tanstack.com/query/latest/docsSuspense
- [9]tanstack.com/query/latest/docsUseSuspenseQuery
- [10]tanstack.com/query/latest/docsUseSuspenseInfiniteQuery
- [11]tanstack.com/query/latest/docsUseQueries
- [12]tanstack.com/query/latest/docsQueryClient
- [13]tanstack.com/query/latest/docsSsr
- [14]tanstack.com/query/latest/docsPrefetching
- [15]tanstack.com/query/latest/docsQueryClient
- [16]tanstack.com/query/latest/docsQueryClient
Source code
- [17]github.com/TanStack/query/blobTanStack/query · useSuspenseQueries.md
- [18]github.com/TanStack/query/blobTanStack/query · suspense.md
- [19]github.com/TanStack/query/blobTanStack/query · QueryClient.md
Other references
- [2]tkdodo.eu/blog/breaking-react-querys-api-on-purposeBreaking React Querys Api On Purpose
Research notes
Curator notes from SOURCES.md captured when the profile was written so you can verify the reasoning, not just the rules.
Sources for @tanstack/react-query Contract
Package: @tanstack/react-query Contract Version: 1.0.0 Last Verified: 2026-02-24
Official Documentation
- Query Functions Guide - Error throwing and handling patterns
- useQuery API Reference - Hook API and error states
- useMutation API Reference - Mutation API and error callbacks
- TypeScript Guide - Error type generics
- Query Retries Guide - Retry configuration and patterns
- Query Retries Guide (Latest) - Updated retry documentation
- Mutations Guide - Mutation error handling
- Infinite Queries Guide - Pagination error scenarios
- Caching Guide - Stale data and gcTime/cacheTime
- Important Defaults - Default behavior understanding
- Optimistic Updates Guide - Rollback patterns
CVE Analysis
CVE-2024-24558
- Title: Cross-site Scripting vulnerability in @tanstack/react-query-next-experimental
- NVD: CVE-2024-24558
- GitHub Advisory: GHSA-997g-27x8-43rf
- Description: XSS vulnerability in experimental Next.js package due to improper handling of untrusted input during server-side rendering
- Fixed In: v5.18.0 or later
- Relevance: Demonstrates importance of proper error handling in SSR contexts
Community Resources
Error Handling Best Practices
- React Query Error Handling - TkDodo Blog - Comprehensive error handling guide
- Global Error Handling in React-Query v5 - Global error handlers with QueryCache
- Error Handling with React Query - TanStack Community - Community discussions
GitHub Issues & Discussions
- Advanced Error Handling Discussion #6490 - Retrieving server error codes
- onError callback behavior with retry #1990 - onError not called if retry enabled
- Global and local onError callbacks #3125 - Error callback patterns
- Retry async behavior #2770 - Async retry challenges
- Should it retry for 404 responses? #372 - Client error retry discussion
Additional Resources
- React Query Retry Strategies - DhiWise - Retry patterns and strategies
- React Query Retry Explained - Dayvster - Detailed retry explanation
- Fixing Tanstack Query Null Error Handling - React Native error handling
- StaleTime vs CacheTime - Medium - Cache management understanding
- StaleTime vs CacheTime Discussion #1685 - Official discussion
- How to Use React Query in React Native - Mobile patterns
Real-World Usage Analysis
jake-tennis-ai-collections Repository
Package Version: ^5.74.3
Configuration Pattern (src/main.tsx):
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error) => {
// Don't retry in dev
if (failureCount >= 0 && import.meta.env.DEV) return false
// Max 3 retries in prod
if (failureCount > 3 && import.meta.env.PROD) return false
// Don't retry auth errors
return !(error instanceof AxiosError && [401, 403].includes(error.response?.status ?? 0))
},
refetchOnWindowFocus: import.meta.env.PROD,
staleTime: 10 * 1000, // 10s
},
mutations: {
onError: async (error) => {
handleServerError(error)
// Track to PostHog
const { captureException } = await import('./lib/posthog')
captureException(error, { error_source: 'mutation', /* ... */ })
// Handle specific errors
if (error instanceof AxiosError) {
if (error.response?.status === 304) {
toast.error('Content not modified!')
}
}
},
},
},
queryCache: new QueryCache({
onError: async (error) => {
// Global query error tracking
const { captureException } = await import('./lib/posthog')
if (error instanceof AxiosError) {
captureException(error, { error_source: 'query_cache', /* ... */ })
// Navigate on auth errors
if (error.response?.status === 401) {
toast.error('Session expired!')
useAuthStore.getState().auth.reset()
router.navigate({ to: '/sign-in', search: { redirect } })
}
// Navigate on server errors
if (error.response?.status === 500) {
toast.error('Internal Server Error!')
router.navigate({ to: '/500' })
}
}
}
}),
})
Key Patterns Observed:
- ✅ Smart retry logic - Doesn't retry client errors (401, 403)
- ✅ Global error handlers - QueryCache.onError for consistent UX
- ✅ Error tracking - Integration with PostHog for monitoring
- ✅ User feedback - Toast notifications for errors
- ✅ Navigation on critical errors - Redirects for 401/500
- ✅ Environment-aware - Different behavior for dev/prod
Notable Implementation:
- Uses Axios error types for HTTP error handling
- Separates mutation errors from query errors with different handlers
- Implements automatic session management on 401 errors
- Configures staleTime to reduce unnecessary refetches
Key Error Behaviors Documented
1. Query Errors (useQuery)
- Error Type: Returned via
errorproperty, not thrown - States:
isError,error,failureReason - Handling Required: Check error state or use ErrorBoundary with throwOnError
- Default Retry: 3 attempts for all errors (should be customized)
2. Mutation Errors (useMutation)
- Error Type: Returned via
errorproperty, not thrown - Handling Required: onError callback, error state check, or mutateAsync().catch()
- Default Retry: None (mutations don't retry by default)
- Optimistic Updates: MUST rollback on error using onMutate context
3. Infinite Query Errors (useInfiniteQuery)
- Error Type: Per-page errors or full query errors
- Handling Required: Check
fetchNextPageErroror globalerror - Refetch Behavior: All pages refetch by default (can fail entirely)
4. Retry Logic Anti-Patterns
- ❌ Bad:
retry: 3(retries client errors like 404) - ❌ Bad:
retry: true(infinite retries) - ✅ Good: Conditional retry based on error type/status
5. Stale Data Handling
- staleTime: Determines data freshness (default 0 - instantly stale)
- gcTime: Memory cleanup time (default 5 minutes)
- Refetch triggers: Window focus, reconnect, mount (configurable)
- Error scenario: Background refetch failures must be surfaced to user
Contract Design Decisions
Severity Levels
ERROR (Must Fix):
- Not handling query/mutation errors at all
- Missing rollback logic for optimistic updates
- Not handling infinite query fetchNextPage errors
- Not handling network errors vs HTTP errors differently
WARNING (Should Fix):
- Retrying on client errors (4xx status codes)
- Missing global error handlers in production
- Using default retry strategy without customization
- Parallel mutations without race condition handling
INFO (Good to Know):
- Edge cases like queryFn returning errors instead of throwing
- Concurrent query error state sharing
- staleTime vs gcTime differences
- Mutation queue patterns
Testing Strategy
Fixtures Created
- proper-error-handling.ts - Correct error handling patterns (0 violations expected)
- missing-error-handling.ts - Missing error handlers (violations expected)
- instance-usage.ts - Real-world usage patterns
Validation Against jake-tennis
- Global error handler patterns ✓
- Retry logic patterns ✓
- Error state checking patterns ✓
- Optimistic update patterns (not used in sample)
Future Considerations
Potential Contract Additions (v2.0.0)
- useQueries hook error handling (batch queries)
- QueryErrorResetBoundary usage patterns
- Suspense mode error handling
- SSR/SSG error handling patterns
- Prefetching error scenarios
- Cache persistence error recovery
Analyzer Enhancements Needed
- Detect missing global error handlers in QueryClient configuration
- Detect retry configuration patterns and validate logic
- Detect optimistic update patterns without rollback
- Track error state usage across components
References
All source URLs have been verified as of 2026-02-24. Contract covers @tanstack/react-query v5.x (latest stable).
Need a different package?
Request a profile