Profiles·Public

@tanstack/react-query

semver>=5.0.0 <6.0.0postconditions30functions16last verified2026-06-24coverage score100%

Postconditions: what we check

  • useQuery · query-error-unhandled
    warning
    WhenqueryFn throws an error and error state is not checked
    ThrowsError (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
    Sources[1][2]
  • useQuery · query-retry-client-errors
    warning
    Whenretry is configured without checking error type (retries on 4xx)
    ThrowsN/A
    Required 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 unavailablevisibilityvisible
    Sources[3]
  • useQuery · stale-query-refetch-error
    error
    Whenstale query refetches in background and new fetch fails
    ThrowsError from queryFn
    Required 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 unavailablevisibilityvisible
    Sources[4]
  • useQuery · network-error-handling
    error
    Whennetwork failure prevents query from executing
    ThrowsNetwork 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 unavailablevisibilityvisible
    Sources[1]
  • useMutation · mutation-error-unhandled
    warning
    WhenmutationFn throws an error and error is not handled
    ThrowsError (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 unavailablevisibilityvisible
    Sources[5]
  • useMutation · mutation-optimistic-update-rollback
    error
    Whenoptimistic update is performed and mutation fails
    ThrowsError from mutationFn
    Required 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 unavailablevisibilityvisible
    Sources[6]
  • useMutation · mutation-default-no-retry
    warning
    Whenmutation fails and caller expects retry behavior
    ThrowsN/A
    Required 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 unavailablevisibilityvisible
    Sources[5]
  • useMutation · mutation-parallel-execution
    warning
    Whenmultiple mutations executing concurrently cause race conditions
    ThrowsN/A
    Required 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 unavailablevisibilityvisible
    Sources[5]
  • useInfiniteQuery · infinite-query-error-unhandled
    warning
    WhenfetchNextPage fails and error is not handled
    ThrowsError from queryFn for failed page
    Required 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 unavailablevisibilityvisible
    Sources[7]
  • useInfiniteQuery · infinite-query-refetch-all-pages
    error
    Whenquery is refetched and middle pages fail
    ThrowsError from queryFn
    Required 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 unavailablevisibilityvisible
    Sources[7]
  • useSuspenseQuery · suspense-query-error-boundary-required
    error
    WhenqueryFn throws any error
    ThrowsError 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
    Sources[8][9]
  • useSuspenseQuery · suspense-query-stale-cache-hides-errors
    warning
    Whenbackground refetch fails but stale cached data exists
    ThrowsError 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 performancevisibilitysilent
    Sources[8]
  • useSuspenseQuery · suspense-cancellation-not-supported
    warning
    Whencomponent unmounts while query is in-flight
    ThrowsCancelledError may not propagate as expected — cancellation does not work with Suspense hooks
    Required 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 performancevisibilitysilent
    Sources[9]
  • useSuspenseInfiniteQuery · suspense-infinite-error-boundary-required
    error
    Whenany page fetch throws an error
    ThrowsError thrown by queryFn — propagated to nearest ErrorBoundary
    Required 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
    Sources[10][8]
  • useQueries · parallel-query-partial-failure
    warning
    Whenone or more queries in the array fail while others succeed
    ThrowsIndividual 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 performancevisibilitysilent
    Sources[11]
  • useQueries · combine-loses-error-info
    error
    Whencombine option is used and error properties are not explicitly forwarded
    ThrowsN/A — errors become inaccessible if combine does not include error properties
    Required 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 datavisibilitysilent
    Sources[11]
  • fetchQuery · fetchquery-throws-on-error
    error
    WhenqueryFn throws an error (network failure, HTTP error, etc.)
    ThrowsError 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
    Sources[12][13]
  • fetchQuery · fetchquery-ssr-uncaught-error
    error
    WhenfetchQuery called during SSR without try-catch
    ThrowsError propagates to SSR framework — causes 500 response or build failure
    Required 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
    Sources[13][14]
  • fetchInfiniteQuery · fetchinfinitequery-throws-on-error
    error
    Whenany page fetch fails
    ThrowsError from queryFn for the failing page
    Required 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 unavailablevisibilityvisible
    Sources[12]
  • ensureQueryData · ensurequerydata-throws-when-fetch-needed
    error
    Whencache miss or stale data triggers a fetch that fails
    ThrowsError from queryFn — thrown when underlying fetch is needed and fails
    Required 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 unavailablevisibilityvisible
    Sources[12]
  • invalidateQueries · invalidatequeries-silent-refetch-failure
    info
    Whenrefetch triggered by invalidation fails and throwOnError is not set
    ThrowsNo 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 performancevisibilitysilent
    Sources[12]
  • refetchQueries · refetchqueries-silent-failure
    warning
    Whenrefetch fails and throwOnError is not set
    ThrowsNo 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 performancevisibilitysilent
    Sources[12]
  • prefetchQuery · prefetchquery-silently-swallows-errors
    warning
    WhenqueryFn throws an error during prefetch
    ThrowsNo error thrown — error is silently discarded
    Required 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 performancevisibilityvisible
    Sources[14]
  • ensureInfiniteQueryData · ensureinfinitequerydata-throws-when-fetch-needed
    error
    Whencache miss or stale data triggers a fetchInfiniteQuery call that fails
    ThrowsError 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
    Sources[15][16]
  • ensureInfiniteQueryData · ensureinfinitequerydata-revalidate-if-stale-silent-background-error
    warning
    WhenrevalidateIfStale: true is set, cache is stale, and the background refetch fails
    ThrowsNo error thrown — stale cached data is returned immediately, background refetch error is silently discarded
    Required 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 performancevisibilitysilent
    Sources[15]
  • useSuspenseQueries · suspense-queries-error-boundary-required
    error
    Whenany query in the array throws an error during fetch
    ThrowsError 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
    Sources[17][18]
  • useSuspenseQueries · suspense-queries-cancellation-not-supported
    warning
    Whencomponent unmounts while one or more queries are in-flight
    ThrowsCancelledError does not propagate as expected — cancellation does not work with Suspense hooks
    Required 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 performancevisibilitysilent
    Sources[17]
  • useSuspenseQueries · suspense-queries-stale-refetch-cascade
    warning
    Whenone query in the array takes significantly longer than others to load on initial mount
    ThrowsN/A — silent re-fetch cascade on re-mount
    Required 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 performancevisibilitysilent
    Sources[17]
  • resetQueries · resetqueries-silent-refetch-failure
    info
    Whenrefetch triggered by reset fails and throwOnError is not set
    ThrowsNo 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 performancevisibilitysilent
    Sources[19]
  • resetQueries · resetqueries-throws-when-throw-on-error
    warning
    Whenoptions.throwOnError is true and any reset-triggered refetch fails
    ThrowsError 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 unavailablevisibilityvisible
    Sources[19]

Sources

Every postcondition cites at least one of these. Grouped by source type; numbered to match the footnotes above.

Official documentation
Source code
Other references

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


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

GitHub Issues & Discussions

Additional Resources


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:

  1. Smart retry logic - Doesn't retry client errors (401, 403)
  2. Global error handlers - QueryCache.onError for consistent UX
  3. Error tracking - Integration with PostHog for monitoring
  4. User feedback - Toast notifications for errors
  5. Navigation on critical errors - Redirects for 401/500
  6. 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 error property, 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 error property, 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 fetchNextPageError or global error
  • 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

  1. proper-error-handling.ts - Correct error handling patterns (0 violations expected)
  2. missing-error-handling.ts - Missing error handlers (violations expected)
  3. 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