0
# Error Handling
1
2
Error boundary integration and reset functionality for graceful error recovery. React Query provides utilities to integrate with React error boundaries and manage error states across queries and mutations.
3
4
## Capabilities
5
6
### QueryErrorResetBoundary Component
7
8
Error boundary context provider that manages error reset functionality across multiple queries.
9
10
```typescript { .api }
11
/**
12
* Provides error reset functionality to child components
13
* @param props - Configuration with children and optional render function
14
* @returns JSX element with error reset context
15
*/
16
function QueryErrorResetBoundary(
17
props: QueryErrorResetBoundaryProps
18
): JSX.Element;
19
20
interface QueryErrorResetBoundaryProps {
21
/** Child components or render function */
22
children:
23
| ((value: QueryErrorResetBoundaryValue) => React.ReactNode)
24
| React.ReactNode;
25
}
26
27
interface QueryErrorResetBoundaryValue {
28
/** Clear the reset flag */
29
clearReset: () => void;
30
/** Check if boundary is in reset state */
31
isReset: () => boolean;
32
/** Trigger a reset */
33
reset: () => void;
34
}
35
```
36
37
**Usage Examples:**
38
39
```typescript
40
import { QueryErrorResetBoundary } from "react-query";
41
42
// Basic error boundary integration
43
function App() {
44
return (
45
<QueryErrorResetBoundary>
46
{({ reset }) => (
47
<ErrorBoundary
48
onReset={reset}
49
fallbackRender={({ error, resetErrorBoundary }) => (
50
<div>
51
<h2>Something went wrong:</h2>
52
<pre>{error.message}</pre>
53
<button onClick={resetErrorBoundary}>Try again</button>
54
</div>
55
)}
56
>
57
<MainContent />
58
</ErrorBoundary>
59
)}
60
</QueryErrorResetBoundary>
61
);
62
}
63
64
// With react-error-boundary library
65
function AppWithErrorBoundary() {
66
return (
67
<QueryErrorResetBoundary>
68
{({ reset }) => (
69
<ErrorBoundary
70
onReset={reset}
71
FallbackComponent={ErrorFallback}
72
>
73
<QueryClientProvider client={queryClient}>
74
<MyApp />
75
</QueryClientProvider>
76
</ErrorBoundary>
77
)}
78
</QueryErrorResetBoundary>
79
);
80
}
81
82
function ErrorFallback({ error, resetErrorBoundary }: {
83
error: Error;
84
resetErrorBoundary: () => void;
85
}) {
86
return (
87
<div role="alert" className="error-fallback">
88
<h2>Oops! Something went wrong</h2>
89
<pre>{error.message}</pre>
90
<button onClick={resetErrorBoundary}>
91
Try again
92
</button>
93
</div>
94
);
95
}
96
```
97
98
### useQueryErrorResetBoundary Hook
99
100
Hook to access error boundary reset functionality.
101
102
```typescript { .api }
103
/**
104
* Access error reset boundary controls
105
* @returns Object with reset control functions
106
*/
107
function useQueryErrorResetBoundary(): QueryErrorResetBoundaryValue;
108
```
109
110
**Usage Examples:**
111
112
```typescript
113
import { useQueryErrorResetBoundary } from "react-query";
114
115
// Manual error reset
116
function ErrorRecoveryComponent() {
117
const { reset, isReset, clearReset } = useQueryErrorResetBoundary();
118
119
const handleRecovery = () => {
120
// Clear any cached errors
121
reset();
122
123
// Perform additional recovery logic
124
queryClient.refetchQueries();
125
126
// Clear the reset flag
127
clearReset();
128
};
129
130
return (
131
<div>
132
<button onClick={handleRecovery}>
133
Recover from Errors
134
</button>
135
{isReset() && (
136
<div className="recovery-notice">
137
Recovery in progress...
138
</div>
139
)}
140
</div>
141
);
142
}
143
144
// Conditional rendering based on reset state
145
function ConditionalErrorHandling() {
146
const { isReset } = useQueryErrorResetBoundary();
147
148
if (isReset()) {
149
return <div>Recovering from errors...</div>;
150
}
151
152
return <MainContent />;
153
}
154
```
155
156
## Advanced Usage Patterns
157
158
### Custom Error Boundary
159
160
Creating a comprehensive error boundary with React Query integration:
161
162
```typescript
163
interface ErrorBoundaryState {
164
hasError: boolean;
165
error: Error | null;
166
errorInfo: React.ErrorInfo | null;
167
}
168
169
class QueryErrorBoundary extends React.Component<
170
{ children: React.ReactNode; onError?: (error: Error, errorInfo: React.ErrorInfo) => void },
171
ErrorBoundaryState
172
> {
173
constructor(props: any) {
174
super(props);
175
this.state = {
176
hasError: false,
177
error: null,
178
errorInfo: null
179
};
180
}
181
182
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
183
return {
184
hasError: true,
185
error
186
};
187
}
188
189
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
190
this.setState({
191
error,
192
errorInfo
193
});
194
195
// Call custom error handler
196
this.props.onError?.(error, errorInfo);
197
198
// Log to error reporting service
199
console.error('Query Error Boundary caught error:', error, errorInfo);
200
}
201
202
render() {
203
if (this.state.hasError) {
204
return (
205
<QueryErrorResetBoundary>
206
{({ reset }) => (
207
<div className="error-boundary">
208
<h2>Something went wrong</h2>
209
<details style={{ whiteSpace: 'pre-wrap' }}>
210
<summary>Error Details</summary>
211
{this.state.error?.toString()}
212
<br />
213
{this.state.errorInfo?.componentStack}
214
</details>
215
<div className="error-actions">
216
<button
217
onClick={() => {
218
reset();
219
this.setState({
220
hasError: false,
221
error: null,
222
errorInfo: null
223
});
224
}}
225
>
226
Try Again
227
</button>
228
<button onClick={() => window.location.reload()}>
229
Reload Page
230
</button>
231
</div>
232
</div>
233
)}
234
</QueryErrorResetBoundary>
235
);
236
}
237
238
return this.props.children;
239
}
240
}
241
```
242
243
### Query-Level Error Handling
244
245
Handling errors at the individual query level:
246
247
```typescript
248
function QueryWithErrorHandling() {
249
const {
250
data,
251
error,
252
isError,
253
refetch,
254
isRefetching
255
} = useQuery({
256
queryKey: ['sensitive-data'],
257
queryFn: fetchSensitiveData,
258
retry: (failureCount, error) => {
259
// Don't retry authentication errors
260
if (error.status === 401) {
261
return false;
262
}
263
// Retry server errors up to 3 times
264
return failureCount < 3;
265
},
266
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
267
onError: (error) => {
268
// Log error for analytics
269
analytics.track('query_error', {
270
queryKey: 'sensitive-data',
271
error: error.message,
272
status: error.status
273
});
274
275
// Handle specific error types
276
if (error.status === 401) {
277
// Redirect to login
278
router.push('/login');
279
} else if (error.status >= 500) {
280
// Show server error toast
281
toast.error('Server error occurred. Please try again.');
282
}
283
},
284
useErrorBoundary: (error) => {
285
// Only throw to error boundary for unexpected errors
286
return error.status >= 500;
287
}
288
});
289
290
// Handle error states in component
291
if (isError && error.status < 500) {
292
return (
293
<div className="query-error">
294
<h3>Unable to load data</h3>
295
<p>{error.message}</p>
296
<button
297
onClick={() => refetch()}
298
disabled={isRefetching}
299
>
300
{isRefetching ? 'Retrying...' : 'Try Again'}
301
</button>
302
</div>
303
);
304
}
305
306
return (
307
<div>
308
{data && <DataDisplay data={data} />}
309
</div>
310
);
311
}
312
```
313
314
### Global Error Handling
315
316
Setting up global error handling across all queries and mutations:
317
318
```typescript
319
// Global error handler
320
const handleGlobalError = (error: any, type: 'query' | 'mutation') => {
321
// Log to error reporting service
322
errorReportingService.captureException(error, {
323
tags: {
324
type,
325
component: 'react-query'
326
}
327
});
328
329
// Handle authentication errors globally
330
if (error.status === 401) {
331
// Clear user data and redirect to login
332
queryClient.setQueryData(['user'], null);
333
router.push('/login');
334
return;
335
}
336
337
// Handle rate limiting
338
if (error.status === 429) {
339
toast.warning('Too many requests. Please wait a moment.');
340
return;
341
}
342
343
// Handle server errors
344
if (error.status >= 500) {
345
toast.error('Server error occurred. Our team has been notified.');
346
return;
347
}
348
349
// Handle network errors
350
if (!navigator.onLine) {
351
toast.warning('You appear to be offline. Please check your connection.');
352
return;
353
}
354
};
355
356
// QueryClient with global error handling
357
const queryClient = new QueryClient({
358
defaultOptions: {
359
queries: {
360
onError: (error) => handleGlobalError(error, 'query'),
361
retry: (failureCount, error) => {
362
// Global retry logic
363
if (error.status === 401 || error.status === 403) {
364
return false;
365
}
366
if (error.status >= 400 && error.status < 500) {
367
return false;
368
}
369
return failureCount < 3;
370
}
371
},
372
mutations: {
373
onError: (error) => handleGlobalError(error, 'mutation'),
374
retry: (failureCount, error) => {
375
// Be more conservative with mutation retries
376
if (error.status >= 400 && error.status < 500) {
377
return false;
378
}
379
return failureCount < 1;
380
}
381
}
382
}
383
});
384
```
385
386
### Error Recovery Strategies
387
388
Implementing different error recovery strategies:
389
390
```typescript
391
function useErrorRecovery() {
392
const queryClient = useQueryClient();
393
const { reset } = useQueryErrorResetBoundary();
394
395
const recoverFromError = useCallback(async (strategy: 'soft' | 'hard' | 'selective') => {
396
switch (strategy) {
397
case 'soft': {
398
// Just reset error boundary
399
reset();
400
break;
401
}
402
403
case 'hard': {
404
// Clear all cache and reset
405
queryClient.clear();
406
reset();
407
break;
408
}
409
410
case 'selective': {
411
// Remove only failed queries
412
queryClient.getQueryCache().getAll().forEach(query => {
413
if (query.state.status === 'error') {
414
queryClient.removeQueries({ queryKey: query.queryKey });
415
}
416
});
417
reset();
418
break;
419
}
420
}
421
}, [queryClient, reset]);
422
423
return { recoverFromError };
424
}
425
426
function ErrorRecoveryPanel() {
427
const { recoverFromError } = useErrorRecovery();
428
const [isRecovering, setIsRecovering] = useState(false);
429
430
const handleRecovery = async (strategy: 'soft' | 'hard' | 'selective') => {
431
setIsRecovering(true);
432
try {
433
await recoverFromError(strategy);
434
toast.success('Recovery completed successfully!');
435
} catch (error) {
436
toast.error('Recovery failed. Please try again.');
437
} finally {
438
setIsRecovering(false);
439
}
440
};
441
442
return (
443
<div className="error-recovery-panel">
444
<h3>Error Recovery Options</h3>
445
<div className="recovery-buttons">
446
<button
447
onClick={() => handleRecovery('soft')}
448
disabled={isRecovering}
449
>
450
Soft Reset
451
</button>
452
<button
453
onClick={() => handleRecovery('selective')}
454
disabled={isRecovering}
455
>
456
Clear Failed Queries
457
</button>
458
<button
459
onClick={() => handleRecovery('hard')}
460
disabled={isRecovering}
461
>
462
Full Reset
463
</button>
464
</div>
465
{isRecovering && <div>Recovering...</div>}
466
</div>
467
);
468
}
469
```
470
471
### Error Monitoring and Analytics
472
473
Comprehensive error monitoring setup:
474
475
```typescript
476
interface ErrorMetrics {
477
totalErrors: number;
478
queryErrors: number;
479
mutationErrors: number;
480
errorsByType: Record<string, number>;
481
recentErrors: Array<{
482
timestamp: number;
483
type: 'query' | 'mutation';
484
error: string;
485
queryKey?: string;
486
}>;
487
}
488
489
function useErrorMonitoring(): ErrorMetrics {
490
const [metrics, setMetrics] = useState<ErrorMetrics>({
491
totalErrors: 0,
492
queryErrors: 0,
493
mutationErrors: 0,
494
errorsByType: {},
495
recentErrors: []
496
});
497
498
const trackError = useCallback((
499
error: Error,
500
type: 'query' | 'mutation',
501
queryKey?: string
502
) => {
503
const errorType = error.name || 'Unknown';
504
505
setMetrics(prev => ({
506
totalErrors: prev.totalErrors + 1,
507
queryErrors: prev.queryErrors + (type === 'query' ? 1 : 0),
508
mutationErrors: prev.mutationErrors + (type === 'mutation' ? 1 : 0),
509
errorsByType: {
510
...prev.errorsByType,
511
[errorType]: (prev.errorsByType[errorType] || 0) + 1
512
},
513
recentErrors: [
514
{
515
timestamp: Date.now(),
516
type,
517
error: error.message,
518
queryKey
519
},
520
...prev.recentErrors.slice(0, 9) // Keep last 10 errors
521
]
522
}));
523
524
// Send to analytics
525
analytics.track('react_query_error', {
526
type,
527
errorType,
528
message: error.message,
529
queryKey,
530
timestamp: Date.now()
531
});
532
}, []);
533
534
// Set up global error tracking
535
useEffect(() => {
536
const queryClient = useQueryClient();
537
538
// Override global error handlers to include tracking
539
const originalQueryOnError = queryClient.getDefaultOptions().queries?.onError;
540
const originalMutationOnError = queryClient.getDefaultOptions().mutations?.onError;
541
542
queryClient.setDefaultOptions({
543
queries: {
544
...queryClient.getDefaultOptions().queries,
545
onError: (error, query) => {
546
trackError(error as Error, 'query', JSON.stringify(query.queryKey));
547
originalQueryOnError?.(error, query);
548
}
549
},
550
mutations: {
551
...queryClient.getDefaultOptions().mutations,
552
onError: (error, variables, context, mutation) => {
553
trackError(error as Error, 'mutation', JSON.stringify(mutation.options.mutationKey));
554
originalMutationOnError?.(error, variables, context, mutation);
555
}
556
}
557
});
558
}, [trackError]);
559
560
return metrics;
561
}
562
563
function ErrorMetricsPanel() {
564
const metrics = useErrorMonitoring();
565
566
return (
567
<div className="error-metrics">
568
<h3>Error Metrics</h3>
569
<div className="metrics-grid">
570
<div className="metric">
571
<span className="label">Total Errors:</span>
572
<span className="value">{metrics.totalErrors}</span>
573
</div>
574
<div className="metric">
575
<span className="label">Query Errors:</span>
576
<span className="value">{metrics.queryErrors}</span>
577
</div>
578
<div className="metric">
579
<span className="label">Mutation Errors:</span>
580
<span className="value">{metrics.mutationErrors}</span>
581
</div>
582
</div>
583
584
<h4>Error Types</h4>
585
<ul>
586
{Object.entries(metrics.errorsByType).map(([type, count]) => (
587
<li key={type}>
588
{type}: {count}
589
</li>
590
))}
591
</ul>
592
593
<h4>Recent Errors</h4>
594
<ul>
595
{metrics.recentErrors.map((error, index) => (
596
<li key={index} className="recent-error">
597
<span className="timestamp">
598
{new Date(error.timestamp).toLocaleTimeString()}
599
</span>
600
<span className="type">[{error.type}]</span>
601
<span className="message">{error.error}</span>
602
</li>
603
))}
604
</ul>
605
</div>
606
);
607
}
608
```