or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

context.mderror-handling.mdindex.mdinfinite-queries.mdmutations.mdparallel-queries.mdqueries.mdssr.mdstatus.md

error-handling.mddocs/

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

```