Skip to main content

Command Palette

Search for a command to run...

Proper Error Handling in React Query

Updated
2 min read
Proper Error Handling in React Query

❌ Wrong → Swallowing Errors

function useRepos() {
  return useQuery({
    queryKey: ['repos'],
    queryFn: async () => {
      try {
        const response = await fetch('/api/repos')
        if (!response.ok) throw new Error('Failed')
        return response.json()
      } catch (error) {
        console.log('Error:', error) // ❌ Swallows the error!
        // React Query never knows an error occurred
      }
    }
  })
}

// Result: status stays 'success', no retries, broken error handling

✅ Correct → Let React Query Handle It

function useRepos() {
  return useQuery({
    queryKey: ['repos'],
    queryFn: async () => {
      const response = await fetch('/api/repos')
      if (!response.ok) {
        throw new Error(`Request failed: ${response.status}`) // ✅ Throws to RQ
      }
      return response.json()
    },
    retry: 3, // Works because error propagates
    retryDelay: 1000
  })
}

Why This Breaks Everything

When you catch and don't re-throw:

  • No retries → React Query doesn't know it failed

  • status stays 'success' → Component thinks everything is fine

  • No error boundaries → Can't use global error handling

  • Silent failures → Users never know something went wrong

The Smart Error Handling Setup

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      throwOnError: (error, query) => {
        // Only throw to Error Boundary if no cached data
        return typeof query.state.data === 'undefined'
      }
    }
  },
  queryCache: new QueryCache({
    onError: (error, query) => {
      // Show toast if we have cached data (background refetch failed)
      if (typeof query.state.data !== 'undefined') {
        toast.error(`Background sync failed: ${error.message}`)
      }
    }
  })
})

This pattern gives you:

  • Error Boundaries for initial load failures

  • Toast notifications for background failures

  • Automatic retries for transient issues

  • Global error handling without component coupling

The key insight: Let React Query own the error lifecycle, then hook into it where you need to.