Actionable rules for designing, implementing, and testing React/Next.js error boundaries with TypeScript.
Your users will never see a blank white screen again. While other developers watch their production apps crumble from a single component failure, you'll have bulletproof error isolation that keeps your UI responsive and your users engaged.
You've been there. A third-party widget crashes. A network request fails unexpectedly. A prop validation breaks. Suddenly your entire React app goes blank, leaving users staring at an empty screen while your error monitoring explodes with complaints.
The traditional approach—wrapping your entire app in one massive try-catch—is like using a sledgehammer for surgery. You lose crucial debugging context, create performance bottlenecks, and still miss the event handlers and async code that slip through the cracks.
These Cursor Rules transform error boundaries from basic crash catchers into a sophisticated fault-tolerance system. Instead of hoping your components won't fail, you'll architect deliberate failure zones that contain damage and guide users to recovery.
Here's what changes in your workflow:
// Before: One boundary to rule them all (and lose them all)
<ErrorBoundary>
<EntireApp />
</ErrorBoundary>
// After: Strategic isolation around failure-prone areas
<Layout>
<AppErrorBoundary> {/* Route-level isolation */}
<DataWidget />
</AppErrorBoundary>
<AppErrorBoundary> {/* Third-party isolation */}
<ExternalChat />
</AppErrorBoundary>
<StableNavigation /> {/* Never wrapped - always accessible */}
</Layout>
Debugging Time: Cut error investigation from hours to minutes with structured component stack traces and Sentry integration that pinpoints exactly which boundary caught what error.
Production Confidence: Deploy fearlessly knowing that widget failures won't cascade. Your checkout flow stays functional even when the recommendation engine crashes.
User Retention: Transform crash experiences into graceful degradation with branded fallback UIs that offer retry options instead of blank screens.
Development Velocity: Write components knowing they can fail safely. No more defensive coding around every API call or prop access.
Before these rules, a failed API response in your analytics widget would crash the entire dashboard. Users lost all their work and context.
// Your new isolated data boundaries
function DashboardPage() {
return (
<div className="dashboard-grid">
<AppErrorBoundary>
<AnalyticsWidget />
</AppErrorBoundary>
<AppErrorBoundary>
<RevenueChart />
</AppErrorBoundary>
{/* Navigation always works */}
<Sidebar />
</div>
)
}
// Each widget fails independently with branded recovery
function ErrorFallback({ error, onRetry }: ErrorFallbackProps) {
return (
<div className="error-widget" role="alert">
<Icon name="warning" />
<h3>Widget temporarily unavailable</h3>
<p>We're having trouble loading this data.</p>
<Button onClick={onRetry}>Try again</Button>
<Button variant="ghost" onClick={() => navigate('/dashboard')}>
Continue without this widget
</Button>
</div>
)
}
Embedding external widgets (chat, analytics, ads) becomes risk-free:
function ProductPage() {
return (
<>
<ProductDetails /> {/* Core functionality protected */}
<AppErrorBoundary>
<ChatWidget /> {/* Can fail without affecting purchase flow */}
</AppErrorBoundary>
<AppErrorBoundary>
<RecommendationEngine />
</AppErrorBoundary>
</>
)
}
Your routing strategy becomes bulletproof with per-route isolation:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="dashboard-layout">
<StableNavigation />
<AppErrorBoundary>
{children} {/* Each route can fail independently */}
</AppErrorBoundary>
</div>
)
}
npm install react-error-boundary @sentry/react
npm install -D @types/react
Create your reusable boundary component:
// src/components/error-boundary/AppErrorBoundary.tsx
import { ErrorBoundary } from 'react-error-boundary'
import * as Sentry from '@sentry/react'
import { ErrorFallback } from './ErrorFallback'
export function AppErrorBoundary({
children
}: {
children: React.ReactNode
}) {
return (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<ErrorFallback error={error} onRetry={resetErrorBoundary} />
)}
onError={(error, info) => {
Sentry.captureException(error, {
extra: { componentStack: info.componentStack }
})
}}
>
{children}
</ErrorBoundary>
)
}
// src/components/error-boundary/ErrorFallback.tsx
interface ErrorFallbackProps {
error: Error
onRetry: () => void
}
export function ErrorFallback({ error, onRetry }: ErrorFallbackProps) {
return (
<div className="error-fallback" role="alert">
<div className="error-content">
<h2>Something went wrong</h2>
<p>We encountered an unexpected error. Your other content is still available.</p>
<button onClick={onRetry} className="retry-button">
Try again
</button>
</div>
</div>
)
}
// src/lib/error/global-handlers.ts
import * as Sentry from '@sentry/react'
export function setupGlobalErrorHandlers() {
window.addEventListener('unhandledrejection', (event) => {
Sentry.captureException(event.reason)
})
window.addEventListener('error', (event) => {
Sentry.captureException(event.error)
})
}
Wrap these component types:
Write focused tests that verify crash recovery:
// __tests__/AppErrorBoundary.test.tsx
test('renders fallback and recovers on retry', async () => {
let shouldError = true
const ProblematicComponent = () => {
if (shouldError) {
throw new Error('Test error')
}
return <div>Success!</div>
}
render(
<AppErrorBoundary>
<ProblematicComponent />
</AppErrorBoundary>
)
// Verify fallback renders
expect(screen.getByRole('alert')).toBeInTheDocument()
expect(screen.getByText(/something went wrong/i)).toBeInTheDocument()
// Test recovery
shouldError = false
fireEvent.click(screen.getByText(/try again/i))
await waitFor(() => {
expect(screen.getByText('Success!')).toBeInTheDocument()
})
})
Error Recovery Rate: Track how often users successfully retry vs. abandon after errors. Expect 60-80% retry success with well-designed fallbacks.
Crash Impact Radius: Measure how many users experience partial vs. complete failures. Properly isolated boundaries reduce complete failures by 90%.
Time to Resolution: Error boundaries with Sentry integration cut debugging time from hours to minutes with precise component stack traces.
User Experience Continuity: Monitor task completion rates during errors. Strategic boundaries maintain 70%+ task completion even during component failures.
Your users will never know how much chaos you're preventing behind the scenes—they'll just experience a robust, professional application that gracefully handles the unexpected. Deploy with confidence knowing that component failures become minor inconveniences instead of app-breaking disasters.
You are an expert in the following technology stack: TypeScript 5+, React 18+, Next.js 14+, react-error-boundary, Sentry, Jest 29 + React Testing Library, Axios, Vite/Webpack.
Key Principles
- Fail gracefully. Never allow a single component failure to take down the whole UI.
- Scope error boundaries narrowly around UI areas prone to failure (data-fetching widgets, third-party embeds, route containers).
- Prefer composition over monolithic global boundaries; stack multiple boundaries when nesting risky components.
- Always couple an error boundary with structured logging (e.g., Sentry) so crashes become actionable.
- Fallback UIs must: 1) explain the issue, 2) offer a recovery path (retry / navigate home), 3) respect brand design.
- Code defensively: validate props, sanitize data, and surface runtime errors early.
TypeScript Rules
- Enable "strict", "noUncheckedIndexedAccess", and "exactOptionalPropertyTypes" in tsconfig.
- All Error objects must extend the built-in `Error` and include a `name`, `message`, and optional `code` field.
- Use discriminated unions for error states returned from hooks (e.g., `{ status:"ok"; data:T } | { status:"error"; error:AppError }`).
- Component props that contain functions should be typed as specific signatures, not `any`.
- Never `throw` non-Error primitives (e.g., `throw "oops"` ➜ ❌).
Error Handling & Validation
- Create one reusable boundary component using `react-error-boundary`:
```tsx
import { ErrorBoundary } from 'react-error-boundary'
import * as Sentry from '@sentry/react'
export function AppErrorBoundary({ children }: { children: React.ReactNode }) {
return (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<ErrorFallback error={error} onRetry={resetErrorBoundary} />
)}
onError={(error, info) => {
Sentry.captureException(error, { extra: info })
}}
>
{children}
</ErrorBoundary>
)
}
```
- Place the happy path last: check for `error` early, return fallback, otherwise render UI.
- Always log `error` & `info.componentStack`.
- For async logic (fetch, mutations), reject promises so boundary can catch errors thrown during render.
- Global safety nets:
```ts
window.addEventListener('unhandledrejection', e => Sentry.captureException(e.reason))
window.addEventListener('error', e => Sentry.captureException(e.error))
```
React/Next.js Rules
- Use functional components; do NOT convert boundaries to hooks only — boundaries must be class or a HOC under the hood.
- In Next.js, wrap each `page.tsx` (or route group) with an error boundary in `layout.tsx` rather than covering the entire `<html>`.
- For `app` router, leverage `error.tsx` for server-side errors and the boundary above for client runtime errors.
- Never perform data fetching inside the boundary component itself; keep it pure.
- Provide suspense boundaries separately; do not mix Suspense and ErrorBoundary in one wrapper — compose them (`<Suspense><ErrorBoundary>…`).
- Forward `resetErrorBoundary` via context when child components need programmatic recovery.
Additional Sections
Testing
- Write one golden test per boundary:
```tsx
test('renders fallback on crash', () => {
const Boom = () => { throw new Error('💥') }
render(
<AppErrorBoundary>
<Boom />
</AppErrorBoundary>
)
expect(screen.getByRole('alert')).toHaveTextContent('Something went wrong')
})
```
- Simulate retries with `fireEvent.click(screen.getByText(/retry/i))` and assert recovery.
- Use mocked Sentry to verify `captureException` called with correct args.
Performance
- Avoid wrapping high-frequency re-rendering leaf components (e.g., animated lists) to reduce React reconciling overhead.
- Memoize fallback UIs; they should be pure, static, and inexpensive to mount.
- Measure bundle impact: ensure boundary component + logging SDK < 10 kB gzipped.
Security
- Never display raw error messages to users. Map internal errors to generic UI copy.
- Strip PII before sending errors to logging providers.
- Sanitize any user-supplied strings rendered in fallback UI.
Folder / File Conventions
- `src/components/error-boundary/`
• `AppErrorBoundary.tsx` (main wrapper)
• `ErrorFallback.tsx` (fallback UI)
• `error.types.ts` (shared types)
- `src/lib/error/`
• `createAppError.ts` (factory util)
• `global-handlers.ts` (window listeners)
Common Pitfalls & Remedies
- Pitfall: Wrapping `<App />` only ➜ Remedy: insert boundaries per route or widget.
- Pitfall: Assuming event-handler errors are caught ➜ Remedy: wrap logic in `try/catch` inside handlers.
- Pitfall: Forgetting to reset state on retry ➜ Remedy: use `resetKeys` prop or call `resetErrorBoundary`.
Checklist Before Merge
[ ] Boundary wraps only necessary subtree.
[ ] Fallback UI meets design spec and offers retry.
[ ] Errors logged to Sentry with component stack.
[ ] Unit & integration tests pass with simulated crash.
[ ] No sensitive data exposed in fallback or logs.