11 KiB
Error Boundaries Implementation (P0 #5)
✅ Status: Complete
Priority: P0 - Critical (Stability)
Effort: 8-12 hours
Date Completed: 2025-11-03
Overview
Error boundaries are React components that catch JavaScript errors in their child component tree, log the errors, and display a fallback UI instead of crashing the entire application.
Before P0 #5: Only 1 error boundary (ModerationErrorBoundary)
After P0 #5: 5 specialized error boundaries covering all critical sections
Error Boundary Architecture
1. RouteErrorBoundary (Top-Level)
Purpose: Last line of defense, wraps all routes
Location: src/components/error/RouteErrorBoundary.tsx
Used in: src/App.tsx - wraps <Routes>
Features:
- Catches route-level errors before they crash the app
- Full-screen error UI with reload/home options
- Critical severity logging
- Minimal UI to ensure maximum stability
Usage:
<RouteErrorBoundary>
<Routes>
{/* All routes */}
</Routes>
</RouteErrorBoundary>
2. AdminErrorBoundary
Purpose: Protects admin panel sections
Location: src/components/error/AdminErrorBoundary.tsx
Used in: Admin routes (/admin/*)
Features:
- Admin-specific error UI with shield icon
- "Back to Dashboard" recovery option
- High-priority error logging
- Section-aware error context
Usage:
<Route
path="/admin/users"
element={
<AdminErrorBoundary section="User Management">
<AdminUsers />
</AdminErrorBoundary>
}
/>
Protected Sections:
- ✅ Dashboard (
/admin) - ✅ Moderation Queue (
/admin/moderation) - ✅ Reports (
/admin/reports) - ✅ System Log (
/admin/system-log) - ✅ User Management (
/admin/users) - ✅ Blog Management (
/admin/blog) - ✅ Settings (
/admin/settings) - ✅ Contact Management (
/admin/contact) - ✅ Email Settings (
/admin/email-settings)
3. EntityErrorBoundary
Purpose: Protects entity detail pages
Location: src/components/error/EntityErrorBoundary.tsx
Used in: Park, Ride, Manufacturer, Designer, Operator, Owner detail routes
Features:
- Entity-aware error messages
- "Back to List" navigation option
- Helpful troubleshooting suggestions
- Graceful degradation
Usage:
<Route
path="/parks/:slug"
element={
<EntityErrorBoundary entityType="park">
<ParkDetail />
</EntityErrorBoundary>
}
/>
Supported Entity Types:
park→ Back to/parksride→ Back to/ridesmanufacturer→ Back to/manufacturersdesigner→ Back to/designersoperator→ Back to/operatorsowner→ Back to/owners
Protected Routes:
- ✅ Park Detail (
/parks/:slug) - ✅ Park Rides (
/parks/:parkSlug/rides) - ✅ Ride Detail (
/parks/:parkSlug/rides/:rideSlug) - ✅ Manufacturer Detail (
/manufacturers/:slug) - ✅ Manufacturer Rides (
/manufacturers/:manufacturerSlug/rides) - ✅ Manufacturer Models (
/manufacturers/:manufacturerSlug/models) - ✅ Model Detail (
/manufacturers/:manufacturerSlug/models/:modelSlug) - ✅ Model Rides (
/manufacturers/:manufacturerSlug/models/:modelSlug/rides) - ✅ Designer Detail (
/designers/:slug) - ✅ Designer Rides (
/designers/:designerSlug/rides) - ✅ Owner Detail (
/owners/:slug) - ✅ Owner Parks (
/owners/:ownerSlug/parks) - ✅ Operator Detail (
/operators/:slug) - ✅ Operator Parks (
/operators/:operatorSlug/parks)
4. ErrorBoundary (Generic)
Purpose: General-purpose error boundary for any component
Location: src/components/error/ErrorBoundary.tsx
Features:
- Context-aware error messages
- Customizable fallback UI
- Optional error callback
- Retry and "Go Home" options
Usage:
import { ErrorBoundary } from '@/components/error';
<ErrorBoundary context="PhotoUpload">
<PhotoUploadForm />
</ErrorBoundary>
// With custom fallback
<ErrorBoundary
context="ComplexChart"
fallback={<p>Failed to load chart</p>}
onError={(error, info) => {
// Custom error handling
analytics.track('chart_error', { error: error.message });
}}
>
<ComplexChart data={data} />
</ErrorBoundary>
5. ModerationErrorBoundary
Purpose: Protects individual moderation queue items
Location: src/components/error/ModerationErrorBoundary.tsx
Status: Pre-existing, retained
Features:
- Item-level error isolation
- Submission ID tracking
- Copy error details functionality
- Prevents one broken item from crashing the queue
Error Boundary Hierarchy
App
├── RouteErrorBoundary (TOP LEVEL - catches everything)
│ └── Routes
│ ├── Admin Routes
│ │ └── AdminErrorBoundary (per admin section)
│ │ └── AdminModeration
│ │ └── ModerationErrorBoundary (per queue item)
│ │
│ ├── Entity Detail Routes
│ │ └── EntityErrorBoundary (per entity page)
│ │ └── ParkDetail
│ │
│ └── Generic Routes
│ └── ErrorBoundary (optional, as needed)
│ └── ComplexComponent
Error Logging
All error boundaries use structured logging via logger.error():
logger.error('Component error caught by boundary', {
context: 'PhotoUpload',
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
url: window.location.href,
userId: user?.id, // If available
});
Log Severity Levels:
RouteErrorBoundary: critical (app-level failure)AdminErrorBoundary: high (admin functionality impacted)EntityErrorBoundary: medium (user-facing page impacted)ErrorBoundary: medium (component failure)ModerationErrorBoundary: medium (queue item failure)
Recovery Options
User Recovery Actions
Each error boundary provides appropriate recovery options:
| Boundary | Actions Available |
|---|---|
| RouteErrorBoundary | Reload Page, Go Home |
| AdminErrorBoundary | Retry, Back to Dashboard, Copy Error |
| EntityErrorBoundary | Try Again, Back to List, Home |
| ErrorBoundary | Try Again, Go Home, Copy Details |
| ModerationErrorBoundary | Retry, Copy Error Details |
Developer Recovery
In development mode, error boundaries show additional debug information:
- ✅ Full error stack trace
- ✅ Component stack trace
- ✅ Error message and context
- ✅ One-click copy to clipboard
Testing Error Boundaries
Manual Testing
- Force a component error:
const BrokenComponent = () => {
throw new Error('Test error boundary');
return <div>This won't render</div>;
};
// Wrap in error boundary
<ErrorBoundary context="Test">
<BrokenComponent />
</ErrorBoundary>
- Test recovery:
- Click "Try Again" → Component should re-render
- Click "Go Home" → Navigate to home page
- Check logs for structured error data
Automated Testing
import { render } from '@testing-library/react';
import { ErrorBoundary } from '@/components/error';
const BrokenComponent = () => {
throw new Error('Test error');
};
test('error boundary catches error and shows fallback', () => {
const { getByText } = render(
<ErrorBoundary context="Test">
<BrokenComponent />
</ErrorBoundary>
);
expect(getByText('Something Went Wrong')).toBeInTheDocument();
expect(getByText('Test error')).toBeInTheDocument();
});
Best Practices
✅ Do
- Wrap lazy-loaded routes with error boundaries
- Use specific error boundaries (Admin, Entity) when available
- Provide context for better error messages
- Log errors with structured data
- Test error boundaries regularly
- Use error boundaries for third-party components
- Add error boundaries around:
- Form submissions
- Data fetching components
- Complex visualizations
- Photo uploads
- Editor components
❌ Don't
- Don't catch errors in event handlers (use try/catch instead)
- Don't use error boundaries for expected errors (validation, 404s)
- Don't nest identical error boundaries
- Don't log sensitive data in error messages
- Don't render without any error boundary (always have at least RouteErrorBoundary)
Common Use Cases
1. Protect Heavy Components
import { ErrorBoundary } from '@/components/error';
<ErrorBoundary context="RichTextEditor">
<MDXEditor content={content} />
</ErrorBoundary>
2. Protect Third-Party Libraries
<ErrorBoundary context="ChartLibrary">
<RechartsLineChart data={data} />
</ErrorBoundary>
3. Protect User-Generated Content Rendering
<ErrorBoundary context="UserBio">
<ReactMarkdown>{user.bio}</ReactMarkdown>
</ErrorBoundary>
4. Protect Form Sections
<ErrorBoundary context="ParkDetailsSection">
<ParkDetailsForm />
</ErrorBoundary>
<ErrorBoundary context="ParkLocationSection">
<ParkLocationForm />
</ErrorBoundary>
Integration with Monitoring (Future)
Error boundaries are designed to integrate with error tracking services:
// Future: Sentry integration
import * as Sentry from '@sentry/react';
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Automatically sent to Sentry
Sentry.captureException(error, {
contexts: {
react: {
componentStack: errorInfo.componentStack,
},
},
tags: {
errorBoundary: this.props.context,
},
});
}
Metrics
Coverage
| Category | Before P0 #5 | After P0 #5 | Status |
|---|---|---|---|
| Admin routes | 0% | 100% (9/9 routes) | ✅ Complete |
| Entity detail routes | 0% | 100% (14/14 routes) | ✅ Complete |
| Top-level routes | 0% | 100% | ✅ Complete |
| Queue items | 100% | 100% | ✅ Maintained |
Impact
- Before: Any component error could crash the entire app
- After: Component errors are isolated and recoverable
- User Experience: Users see helpful error messages with recovery options
- Developer Experience: Better error logging with full context
Related Documentation
- P0 #2: Console Statement Prevention →
docs/LOGGING_POLICY.md - P0 #4: Hardcoded Secrets Removal → (completed)
- Error Handling Patterns →
src/lib/errorHandler.ts - Logger Implementation →
src/lib/logger.ts
Maintenance
Adding a New Error Boundary
- Identify the component/section that needs protection
- Choose appropriate error boundary type:
- Admin section? →
AdminErrorBoundary - Entity page? →
EntityErrorBoundary - Generic component? →
ErrorBoundary
- Admin section? →
- Wrap the component in the route definition or parent component
- Provide context for better error messages
- Test the error boundary manually
Updating Existing Boundaries
- Keep error messages user-friendly
- Don't expose stack traces in production
- Ensure recovery actions work correctly
- Update tests when changing boundaries
Summary
✅ 5 error boundary types covering all critical sections
✅ 100% admin route coverage (9/9 routes)
✅ 100% entity route coverage (14/14 routes)
✅ Top-level protection via RouteErrorBoundary
✅ User-friendly error UIs with recovery options
✅ Structured error logging for debugging
✅ Development mode debugging with stack traces
Result: Application is significantly more stable and resilient to component errors. Users will never see a blank screen due to a single component failure.