Error Boundaries in React
1. Executive Summary & Strategic Necessity
1.1 Context (ภาษาไทย)
Error Boundaries เป็นคุณลักษณะที่สำคัญใน React ที่ช่วยจัดการข้อผิดพลาด (Error Handling) ในแอปพลิเคชัน React โดยการจับ JavaScript errors ที่เกิดขึ้นใน component tree และแสดง fallback UI แทนที่จะทำให้ทั้งแอปพลิเคชัน crash นี่เป็นสิ่งสำคัญในการสร้างแอปพลิเคชันที่มีความเสถียรและให้ประสบการณ์ผู้ใช้ที่ดี
Error Boundaries ถูกนำเสนอใน React 16 และเป็นส่วนสำคัญของการสร้างแอปพลิเคชัน React ที่มีความทนทาน (Resilient) และสามารถกู้คืนจากข้อผิดพลาดได้อย่างสวยงาม
1.2 Business Impact (ภาษาไทย)
ผลกระทบทางธุรกิจ:
-
ลด Downtime - Error Boundaries ช่วยลดเวลาที่แอปพลิเคชันไม่สามารถใช้งานได้เนื่องจากข้อผิดพลาด โดยการจับและจัดการข้อผิดพลาดในระดับ component แทนที่จะทำให้ทั้งแอป crash
-
เพิ่ม User Retention - ผู้ใช้มักจะออกจากแอปพลิเคชันที่มีข้อผิดพลาดบ่อย Error Boundaries ช่วยให้แอปพลิเคชันยังคงใช้งานได้แม้จะมีข้อผิดพลาดในบางส่วน
-
ลด Support Cost - Error Reporting ที่ดีช่วยให้ทีมพัฒนาสามารถระบุและแก้ไขปัญหาได้เร็วขึ้น ทำให้ลดคำถามและปัญหาจากผู้ใช้
-
เพิ่ม Trust - แอปพลิเคชันที่มีการจัดการข้อผิดพลาดที่ดีสร้างความไว้วางใจให้กับผู้ใช้
-
ปรับปรุง User Experience - Fallback UI ที่ดีช่วยให้ผู้ใช้เข้าใจสถานการณ์และมีตัวเลือกในการกู้คืน
1.3 Product Thinking (ภาษาไทย)
มุมมองด้านผลิตภัณฑ์:
-
Graceful Degradation - Error Boundaries ช่วยให้แอปพลิเคชันสามารถทำงานได้แม้จะมีบางส่วนที่ไม่ทำงาน ผู้ใช้ยังสามารถใช้งานส่วนอื่นๆ ของแอปพลิเคชันได้
-
Error Recovery - ให้ผู้ใช้มีตัวเลือกในการกู้คืนจากข้อผิดพลาด เช่น Retry, Go Home, Contact Support
-
Contextual Error Messages - Error messages ต้องเข้าใจง่ายและให้ข้อมูลที่เป็นประโยชน์แก่ผู้ใช้
-
Error Tracking - Error reporting ต้องมี context เพียงพอสำหรับการ debug เช่น component stack, user info, environment
-
User-Centric Design - Fallback UI ต้องออกแบบให้ใช้งานง่ายและให้ผู้ใช้มีความรู้สึกดีแม้จะเกิดข้อผิดพลาด
2. Technical Deep Dive (The "How-to")
2.1 Core Logic
Error Boundaries คือ React components ที่:
- จับ JavaScript errors ใน component tree ของตนเอง
- Log errors สำหรับการ debug
- แสดง fallback UI แทน component tree ที่ crashed
สิ่งที่ Error Boundaries จับได้:
- Rendering errors
- Errors ใน lifecycle methods
- Errors ใน constructors
สิ่งที่ Error Boundaries ไม่จับได้:
- Event handlers
- Asynchronous code (setTimeout, promises)
- Server-side rendering errors
- Errors ที่เกิดใน error boundary เอง
2.2 Architecture Diagram Requirements
┌─────────────────────────────────────────────────────────────────┐
│ Error Boundary Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Global Error Boundary │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ App Component │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ │ │
│ │ │ │ Header │ │ Feature 1 │ │ Footer │ │ │ │
│ │ │ └─────────────┘ └──────┬──────┘ └───────────┘ │ │ │
│ │ │ │ │ │ │
│ │ │ ┌─────────────────────▼──────────────────────┐ │ │ │
│ │ │ │ Feature Error Boundary │ │ │ │
│ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │
│ │ │ │ │ Component A │ │ Component B │ │ │ │
│ │ │ │ └─────────────┘ └─────────────┘ │ │ │
│ │ │ │ │ │ │
│ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │
│ │ │ │ │ Component C │ │ Component D │ │ │ │
│ │ │ │ └─────────────┘ └─────────────┘ │ │ │
│ │ │ └───────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────────────────────────────┐ │ │ │
│ │ │ │ Error Reporting Service │ │ │ │
│ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │
│ │ │ │ │ Sentry │ │LogRocket│ │ Custom │ │ │ │
│ │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │
│ │ │ └─────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Fallback UI Layer │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Simple │ │ Detailed │ │ Themed │ │ │
│ │ │ Fallback │ │ Fallback │ │ Fallback │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2.3 Implementation Workflow
Step 1: Create Base Error Boundary Component
// components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react'
interface Props {
children: ReactNode
fallback?: ReactNode
onError?: (error: Error, errorInfo: ErrorInfo) => void
}
interface State {
hasError: boolean
error: Error | null
errorInfo: ErrorInfo | null
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { hasError: false, error: null, errorInfo: null }
}
static getDerivedStateFromError(error: Error): State {
// Update state so the next render will show the fallback UI
return { hasError: true, error, errorInfo: null }
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Log the error to an error reporting service
console.error('Error caught by boundary:', error, errorInfo)
// Call custom error handler
this.props.onError?.(error, errorInfo)
// Store error info for display
this.setState({ errorInfo })
}
resetError = () => {
this.setState({ hasError: false, error: null, errorInfo: null })
}
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback
}
// Default fallback
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
{this.state.error && (
<p className="error-message">{this.state.error.message}</p>
)}
<button onClick={this.resetError}>Try Again</button>
</div>
)
}
return this.props.children
}
}
Step 2: Create Error Reporting Service
// services/ErrorReportingService.ts
import * as Sentry from '@sentry/react'
export class ErrorReportingService {
static async report(error: Error, errorInfo?: ErrorInfo, context?: Record<string, any>) {
// Send to Sentry
Sentry.withScope((scope) => {
if (errorInfo) {
scope.setExtra('componentStack', errorInfo.componentStack)
}
// Add custom context
if (context) {
Object.entries(context).forEach(([key, value]) => {
scope.setExtra(key, value)
})
}
scope.setTag('errorBoundary', 'true')
Sentry.captureException(error)
})
// Also log to console in development
if (process.env.NODE_ENV === 'development') {
console.error('Error reported:', error, errorInfo, context)
}
}
static setUser(user: { id: string; email?: string; username?: string }) {
Sentry.setUser(user)
}
static setTag(key: string, value: string) {
Sentry.setTag(key, value)
}
static clearUser() {
Sentry.setUser(null)
}
}
Step 3: Create Fallback UI Components
// components/ErrorFallback.tsx
import React from 'react'
interface SimpleFallbackProps {
error?: Error | null
onReset?: () => void
}
export function SimpleFallback({ error, onReset }: SimpleFallbackProps) {
return (
<div className="error-boundary simple">
<h2>Something went wrong</h2>
<p>We're sorry for the inconvenience.</p>
{error && <p className="error-message">{error.message}</p>}
{onReset && <button onClick={onReset}>Try Again</button>}
</div>
)
}
interface DetailedFallbackProps {
error?: Error | null
errorInfo?: ErrorInfo | null
onReset?: () => void
eventId?: string
}
export function DetailedFallback({ error, errorInfo, onReset, eventId }: DetailedFallbackProps) {
return (
<div className="error-boundary detailed">
<div className="error-icon">⚠️</div>
<h2>Oops! Something went wrong</h2>
<div className="error-details">
<h3>Error Message</h3>
<p>{error?.message || 'Unknown error'}</p>
{errorInfo && (
<details>
<summary>Technical Details</summary>
<pre>{errorInfo.componentStack}</pre>
</details>
)}
</div>
<div className="error-actions">
{onReset && (
<button onClick={onReset} className="btn-primary">
Try Again
</button>
)}
<button onClick={() => window.location.href = '/'} className="btn-secondary">
Go Home
</button>
<button onClick={() => window.location.reload()} className="btn-tertiary">
Reload Page
</button>
</div>
{eventId && (
<p className="error-event-id">
Error ID: <code>{eventId}</code>
</p>
)}
<p className="error-contact">
Need help? <a href="/support">Contact Support</a>
</p>
</div>
)
}
Step 4: Implement Placement Strategy
// App.tsx
import { ErrorBoundary } from './components/ErrorBoundary'
import { DetailedFallback } from './components/ErrorFallback'
import { ErrorReportingService } from './services/ErrorReportingService'
function App() {
return (
// Global boundary for catastrophic errors
<ErrorBoundary
fallback={<DetailedFallback />}
onError={(error, errorInfo) => {
ErrorReportingService.report(error, errorInfo, {
location: 'global',
timestamp: new Date().toISOString(),
})
}}
>
<div className="app-layout">
<Header />
<main>
{/* Feature-specific boundaries */}
<ErrorBoundary
fallback={
<DetailedFallback
error={null}
onReset={() => window.location.href = '/dashboard'}
/>
}
onError={(error, errorInfo) => {
ErrorReportingService.report(error, errorInfo, {
location: 'dashboard',
feature: 'dashboard',
})
}}
>
<Dashboard />
</ErrorBoundary>
<ErrorBoundary
fallback={
<DetailedFallback
error={null}
onReset={() => window.location.href = '/projects'}
/>
}
onError={(error, errorInfo) => {
ErrorReportingService.report(error, errorInfo, {
location: 'projects',
feature: 'projects',
})
}}
>
<Projects />
</ErrorBoundary>
</main>
<Footer />
</div>
</ErrorBoundary>
)
}
3. Tooling & Tech Stack
3.1 Enterprise Tools
| Tool | Purpose | Version | License |
|---|---|---|---|
| React | UI Library | ^18.0.0 | MIT |
| react-error-boundary | Error Boundary Library | ^4.0.0 | MIT |
| Sentry | Error Tracking | ^7.0.0 | Commercial/Free |
| LogRocket | Session Recording | ^3.0.0 | Commercial |
| TypeScript | Type Safety | ^5.0.0 | Apache 2.0 |
3.2 Configuration Essentials
Sentry Setup:
// sentry.ts
import * as Sentry from '@sentry/react'
import { BrowserTracing } from '@sentry/tracing'
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,
release: process.env.NEXT_PUBLIC_APP_VERSION,
integrations: [
new BrowserTracing(),
new Sentry.Replay({
maskAllText: true,
blockAllMedia: true,
}),
],
tracesSampleRate: 0.1,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
beforeSend(event) {
// Filter out sensitive data
if (event.request?.cookies) {
delete event.request.cookies
}
return event
},
})
react-error-boundary Setup:
npm install react-error-boundary
// ErrorBoundaryProvider.tsx
import { ErrorBoundary } from 'react-error-boundary'
export function AppErrorBoundary({ children }: { children: React.ReactNode }) {
const logError = (error: Error, info: { componentStack: string }) => {
console.error('Error caught by boundary:', error, info)
// Send to error tracking service
ErrorReportingService.report(error, info)
}
return (
<ErrorBoundary
FallbackComponent={DetailedFallback}
onError={logError}
onReset={() => {
// Optional: reset app state
window.location.href = '/'
}}
>
{children}
</ErrorBoundary>
)
}
4. Standards, Compliance & Security
4.1 International Standards
- WCAG 2.1 Level AA - Error messages ต้องเข้าถึงได้และเข้าใจง่าย
- ISO 25010 - Quality Model สำหรับ Reliability และ Recoverability
- OWASP - Error Handling Best Practices สำหรับ Security
4.2 Security Protocol
Error Boundaries ต้องปฏิบัติตามหลักความปลอดภัย:
- No Sensitive Data in Errors - ไม่แสดงข้อมูลที่ละเอียดอ่อนใน error messages
- Error Logging - Log errors อย่างปลอดภัยโดยไม่เก็บข้อมูลส่วนบุคคล
- Sanitization - Sanitize error messages ก่อนแสดงใน UI
- Rate Limiting - จำกัดจำนวน errors ที่ส่งไปยัง error tracking service
// Secure Error Reporting
export class SecureErrorReportingService {
static sanitizeError(error: Error): Error {
// Remove sensitive data from error message
const sanitizedMessage = error.message
.replace(/password=["'][^"']*["']/gi, 'password="***"')
.replace(/token=["'][^"']*["']/gi, 'token="***"')
.replace(/api[_-]?key=["'][^"']*["']/gi, 'api_key="***"')
return new Error(sanitizedMessage)
}
static report(error: Error, errorInfo?: ErrorInfo) {
const sanitizedError = this.sanitizeError(error)
// Send to error tracking service
ErrorReportingService.report(sanitizedError, errorInfo)
}
}
4.3 Explainability
Error Boundaries ต้องสามารถอธิบายได้ว่า:
- Error Context - Error เกิดขึ้นที่ไหนและเมื่อไร
- User Impact - Error นี้ส่งผลกระทบต่อผู้ใช้อย่างไร
- Recovery Path - ผู้ใช้สามารถกู้คืนจาก error ได้อย่างไร
- Debug Information - ข้อมูลที่จำเป็นสำหรับการ debug
5. Unit Economics & Performance Metrics (KPIs)
5.1 Cost Calculation
| Metric | Calculation | Target |
|---|---|---|
| Error Rate | Errors / Total Requests | < 0.1% |
| Crash Rate | Crashes / Total Sessions | < 0.01% |
| Recovery Rate | Recovered Errors / Total Errors | > 95% |
| MTTR (Mean Time To Recovery) | Average recovery time | < 5 min |
| Error Reporting Latency | Time to report error | < 1s |
5.2 Key Performance Indicators
Technical Metrics:
- Error Rate - จำนวน errors ต่อจำนวน requests
- Crash Rate - จำนวน crashes ต่อจำนวน sessions
- Recovery Rate - อัตราการกู้คืนจาก errors
- Error Reporting Latency - เวลาในการ report error
Business Metrics:
- User Retention - อัตราการกลับมาใช้งานหลังจากเกิด error
- Support Tickets - จำนวน support tickets ที่เกี่ยวข้องกับ errors
- User Satisfaction - ความพึงพอใจของผู้ใช้หลังจากเกิด error
- Time to Resolution - เวลาในการแก้ไข errors
6. Strategic Recommendations (CTO Insights)
6.1 Phase Rollout
Phase 1: Foundation (Week 1-2)
- Create base Error Boundary component
- Setup error reporting service (Sentry)
- Create fallback UI components
- Implement global error boundary
Phase 2: Feature-Level Boundaries (Week 3-4)
- Add feature-level error boundaries
- Implement contextual fallback UIs
- Add error reporting for each feature
- Test error boundaries with intentional errors
Phase 3: Advanced Features (Week 5-6)
- Implement retry mechanisms
- Add error rate monitoring
- Create error recovery strategies
- Implement error boundary composition
Phase 4: Optimization (Week 7-8)
- Performance audit
- Error rate analysis
- User feedback collection
- Documentation and training
6.2 Pitfalls to Avoid
- Over-Engineering - Error boundaries มากเกินไปทำให้โค้ดซับซ้อน
- Ignoring Non-Catchable Errors - ไม่จัดการ async errors และ event handler errors
- Poor Fallback UI - Fallback UI ที่ไม่เป็นประโยชน์สำหรับผู้ใช้
- Missing Error Context - Error reporting ไม่มี context เพียงพอ
- No Recovery Mechanism - ไม่มีวิธีให้ผู้ใช้กู้คืนจาก errors
6.3 Best Practices Checklist
- ใช้ global error boundary สำหรับ catastrophic errors
- ใช้ feature-level error boundaries สำหรับ better UX
- Integrate กับ error tracking service (Sentry, LogRocket)
- Provide fallback UI ที่ชัดเจนและเป็นประโยชน์
- Implement retry mechanisms
- Log errors ด้วย context เพียงพอ
- Test error boundaries ด้วย intentional errors
- Monitor error rates และ set up alerts
- Sanitize error messages ก่อนแสดงใน UI
- Document error boundary patterns
7. Implementation Examples
7.1 Basic Error Boundary
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
// Update state so next render shows fallback UI
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return <FallbackUI error={this.state.error} />;
}
return this.props.children;
}
}
7.2 Error Boundary with componentDidCatch
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null, errorInfo: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log error to an error reporting service
logErrorToService(error, errorInfo);
// Store error info for display
this.setState({ errorInfo });
}
render() {
if (this.state.hasError) {
return <FallbackUI error={this.state.error} info={this.state.errorInfo} />;
}
return this.props.children;
}
}
function logErrorToService(error, errorInfo) {
// Send to Sentry, LogRocket, etc.
Sentry.captureException(error, {
contexts: {
react: {
componentStack: errorInfo.componentStack,
},
},
});
}
7.3 Complete Error Boundary Example
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
// Update state to render fallback UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Log error details
console.error('Error caught by boundary:', error, errorInfo);
// Send to error tracking service
this.logError(error, errorInfo);
// Store error info
this.setState({
error,
errorInfo,
});
}
logError(error, errorInfo) {
// Example: Send to Sentry
if (typeof Sentry !== 'undefined') {
Sentry.withScope((scope) => {
scope.setExtra('componentStack', errorInfo.componentStack);
Sentry.captureException(error);
});
}
}
render() {
if (this.state.hasError) {
return this.props.fallback({
error: this.state.error,
errorInfo: this.state.errorInfo,
resetError: this.resetError.bind(this),
});
}
return this.props.children;
}
resetError() {
this.setState({ hasError: false, error: null, errorInfo: null });
}
}
// Usage
<ErrorBoundary
fallback={({ error, resetError }) => (
<div className="error-fallback">
<h2>Something went wrong</h2>
<p>{error?.message}</p>
<button onClick={resetError}>Try Again</button>
</div>
)}
>
<MyComponent />
</ErrorBoundary>
7.4 Error Boundary Limitations
class MyComponent extends React.Component {
handleClick() {
// Event handlers are NOT caught
try {
throw new Error('Event handler error');
} catch (error) {
// Must handle manually
console.error(error);
}
}
async componentDidMount() {
// Async errors are NOT caught
try {
const data = await fetchData(); // Error here won't be caught
} catch (error) {
// Must handle manually
console.error(error);
}
// setTimeout errors are NOT caught
setTimeout(() => {
throw new Error('Timeout error'); // Won't be caught
}, 1000);
}
render() {
return <button onClick={this.handleClick}>Click me</button>;
}
}
7.5 Handling Non-Catchable Errors
// For async errors, wrap in try-catch
class MyComponent extends React.Component {
state = { data: null, error: null };
async componentDidMount() {
try {
const data = await fetchData();
this.setState({ data });
} catch (error) {
this.setState({ error });
// Log error
logError(error);
}
}
render() {
if (this.state.error) {
return <ErrorUI error={this.state.error} />;
}
if (!this.state.data) {
return <LoadingUI />;
}
return <DataUI data={this.state.data} />;
}
}
7.6 Placement Strategies
Granular (Feature-Level):
function App() {
return (
<div>
<ErrorBoundary fallback={<ProfileError />}>
<ProfileSection />
</ErrorBoundary>
<ErrorBoundary fallback={<FeedError />}>
<FeedSection />
</ErrorBoundary>
<ErrorBoundary fallback={<SettingsError />}>
<SettingsSection />
</ErrorBoundary>
</div>
);
}
Global (App-Level):
function App() {
return (
<ErrorBoundary fallback={<GlobalError />}>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Router>
</ErrorBoundary>
);
}
Hybrid Approach:
function App() {
return (
// Global boundary for catastrophic errors
<ErrorBoundary fallback={<GlobalError />}>
<div className="app-layout">
<Header />
<main>
{/* Feature-specific boundaries */}
<ErrorBoundary fallback={<DashboardError />}>
<Dashboard />
</ErrorBoundary>
<ErrorBoundary fallback={<ProjectsError />}>
<Projects />
</ErrorBoundary>
</main>
<Footer />
</div>
</ErrorBoundary>
);
}
7.7 Fallback UI Patterns
Simple Fallback:
function SimpleFallback({ error, resetError }) {
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>We're sorry for inconvenience.</p>
{error && <p className="error-message">{error.message}</p>}
<button onClick={resetError}>Try Again</button>
</div>
);
}
Detailed Fallback:
function DetailedFallback({ error, errorInfo, resetError }) {
return (
<div className="error-boundary detailed">
<div className="error-icon">⚠️</div>
<h2>Oops! Something went wrong</h2>
<div className="error-details">
<h3>Error Message</h3>
<p>{error?.message || 'Unknown error'}</p>
{errorInfo && (
<details>
<summary>Technical Details</summary>
<pre>{errorInfo.componentStack}</pre>
</details>
)}
</div>
<div className="error-actions">
<button onClick={resetError} className="btn-primary">
Try Again
</button>
<button onClick={() => window.location.href = '/'} className="btn-secondary">
Go Home
</button>
<button onClick={() => window.location.reload()} className="btn-tertiary">
Reload Page
</button>
</div>
<p className="error-contact">
Need help? <a href="/support">Contact Support</a>
</p>
</div>
);
}
Themed Fallback:
function ThemedFallback({ error, resetError }) {
const theme = useTheme();
return (
<div className={`error-boundary ${theme}`}>
<div className="error-container">
<div className="error-illustration">
{theme === 'dark' ? (
<DarkErrorIllustration />
) : (
<LightErrorIllustration />
)}
</div>
<h2>Something went wrong</h2>
<p>{error?.message}</p>
<button onClick={resetError} className="btn">
Try Again
</button>
</div>
</div>
);
}
7.8 Error Reporting Integration
Sentry Integration:
import * as Sentry from '@sentry/react';
class SentryErrorBoundary extends React.Component {
state = { eventId: null };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Send error to Sentry
Sentry.withScope((scope) => {
scope.setExtra('componentStack', errorInfo.componentStack);
scope.setTag('errorBoundary', 'true');
const eventId = Sentry.captureException(error);
this.setState({ eventId });
});
}
render() {
if (this.state.hasError) {
return (
<FallbackUI
eventId={this.state.eventId}
resetError={() => window.location.reload()}
/>
);
}
return this.props.children;
}
}
// Initialize Sentry
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
release: process.env.APP_VERSION,
});
LogRocket Integration:
import LogRocket from 'logrocket';
class LogRocketErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
// Log error to LogRocket
LogRocket.captureException(error, {
tags: {
errorBoundary: 'true',
},
extra: {
componentStack: errorInfo.componentStack,
},
});
}
render() {
if (this.state.hasError) {
return <FallbackUI />;
}
return this.props.children;
}
}
// Initialize LogRocket
LogRocket.init('YOUR_APP_ID', {
release: process.env.APP_VERSION,
console: {
isEnabled: process.env.NODE_ENV === 'development',
},
});
Custom Error Service:
class ErrorReportingService {
static async report(error, errorInfo) {
const payload = {
message: error.message,
stack: error.stack,
componentStack: errorInfo?.componentStack,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
};
try {
await fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
} catch (reportError) {
console.error('Failed to report error:', reportError);
}
}
}
class ReportingErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
ErrorReportingService.report(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <FallbackUI />;
}
return this.props.children;
}
}
7.9 Retry Mechanisms
Simple Retry:
function RetryableComponent() {
const [error, setError] = useState(null);
const [retryCount, setRetryCount] = useState(0);
const handleRetry = () => {
setError(null);
setRetryCount(prev => prev + 1);
};
return (
<ErrorBoundary
fallback={
<div className="error-fallback">
<h2>Error occurred</h2>
<p>{error?.message}</p>
<button onClick={handleRetry}>
Retry ({retryCount})
</button>
</div>
}
onError={setError}
>
<ChildComponent key={retryCount} />
</ErrorBoundary>
);
}
Exponential Backoff Retry:
function ExponentialRetryComponent() {
const [error, setError] = useState(null);
const [retryCount, setRetryCount] = useState(0);
const [isRetrying, setIsRetrying] = useState(false);
const handleRetry = async () => {
setIsRetrying(true);
setError(null);
// Exponential backoff
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000);
await new Promise(resolve => setTimeout(resolve, delay));
setRetryCount(prev => prev + 1);
setIsRetrying(false);
};
return (
<ErrorBoundary
fallback={
<div className="error-fallback">
<h2>Error occurred</h2>
<p>{error?.message}</p>
{isRetrying ? (
<button disabled>Retrying...</button>
) : (
<button onClick={handleRetry}>
Retry ({retryCount})
</button>
)}
</div>
}
onError={setError}
>
<ChildComponent key={retryCount} />
</ErrorBoundary>
);
}
Smart Retry:
function SmartRetryComponent() {
const [error, setError] = useState(null);
const [retryCount, setRetryCount] = useState(0);
const maxRetries = 3;
const shouldRetry = error => {
// Only retry on specific errors
const retryableErrors = [
'NetworkError',
'TimeoutError',
'ServiceUnavailable',
];
return retryableErrors.some(type => error?.name?.includes(type));
};
const handleRetry = () => {
setError(null);
setRetryCount(prev => prev + 1);
};
return (
<ErrorBoundary
fallback={
<div className="error-fallback">
<h2>Error occurred</h2>
<p>{error?.message}</p>
{shouldRetry(error) && retryCount < maxRetries ? (
<button onClick={handleRetry}>
Retry ({retryCount}/{maxRetries})
</button>
) : (
<p>Please try again later or contact support.</p>
)}
</div>
}
onError={setError}
>
<ChildComponent key={retryCount} />
</ErrorBoundary>
);
}
7.10 Error Boundaries with React Query/SWR
React Query Error Boundaries:
import { useQuery, QueryErrorResetBoundary } from '@tanstack/react-query';
function UserProfile() {
const { data, error, isLoading } = useQuery({
queryKey: ['user', 'profile'],
queryFn: fetchUserProfile,
retry: 3,
});
if (isLoading) return <Loading />;
if (error) return <ErrorUI error={error} />;
return <Profile data={data} />;
}
// With Error Boundary
function App() {
return (
<ErrorBoundary fallback={<GlobalError />}>
<QueryErrorResetBoundary>
<UserProfile />
</QueryErrorResetBoundary>
</ErrorBoundary>
);
}
SWR Error Boundaries:
import useSWR from 'swr';
function UserProfile() {
const { data, error, isLoading } = useSWR('/api/user/profile', fetcher);
if (isLoading) return <Loading />;
if (error) return <ErrorUI error={error} />;
return <Profile data={data} />;
}
// With Error Boundary
function App() {
return (
<ErrorBoundary fallback={<GlobalError />}>
<SWRConfig value={{ onError: handleError }}>
<UserProfile />
</SWRConfig>
</ErrorBoundary>
);
}
function handleError(error) {
// Log error
console.error('SWR Error:', error);
// Show notification
toast.error('Failed to load data');
}
Combined Approach:
function DataComponent() {
const { data, error, isLoading, refetch } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
});
if (isLoading) return <Loading />;
if (error) {
// Let error boundary handle it
throw error;
}
return <DataView data={data} />;
}
function App() {
return (
<ErrorBoundary
fallback={
<ErrorFallback
onRetry={() => window.location.reload()}
/>
}
>
<DataComponent />
</ErrorBoundary>
);
}
7.11 Error Boundaries with Suspense
Suspense + Error Boundary Pattern:
import { Suspense } from 'react';
function ResourceComponent() {
const resource = useResource(resourcePromise);
if (resource.status === 'pending') {
throw resource.promise; // Suspense catches this
}
if (resource.status === 'rejected') {
throw resource.reason; // Error Boundary catches this
}
return <DataView data={resource.result} />;
}
function App() {
return (
<ErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<Loading />}>
<ResourceComponent />
</Suspense>
</ErrorBoundary>
);
}
Multiple Suspense Boundaries:
function App() {
return (
<ErrorBoundary fallback={<GlobalError />}>
<Suspense fallback={<PageLoading />}>
<Header />
<main>
<ErrorBoundary fallback={<ContentError />}>
<Suspense fallback={<SectionLoading />}>
<Dashboard />
</Suspense>
</ErrorBoundary>
<ErrorBoundary fallback={<ContentError />}>
<Suspense fallback={<SectionLoading />}>
<Feed />
</Suspense>
</ErrorBoundary>
</main>
<Footer />
</Suspense>
</ErrorBoundary>
);
}
7.12 Error Boundaries in Server Components (Next.js)
Next.js App Router:
// app/error.js
'use client';
export default function Error({
error,
reset,
}) {
return (
<div className="error-page">
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/global-error.js
'use client';
export default function GlobalError({ error }) {
return (
<html>
<body>
<div className="global-error">
<h2>Application Error</h2>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>
Reload Application
</button>
</div>
</body>
</html>
);
}
Next.js Pages Router:
// pages/_error.js
class ErrorPage extends React.Component {
static getInitialProps({ res, err }) {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
return { statusCode };
}
render() {
const { statusCode } = this.props;
return (
<div className="error-page">
<h1>{statusCode}</h1>
<p>Something went wrong</p>
<Link href="/">Go Home</Link>
</div>
);
}
}
export default ErrorPage;
Component-Level Error Boundary:
'use client';
class ComponentErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error in component:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <ErrorFallback />;
}
return this.props.children;
}
}
// Usage in Next.js component
export default function MyComponent() {
return (
<ComponentErrorBoundary>
<Content />
</ComponentErrorBoundary>
);
}
7.13 react-error-boundary Library
Installation:
npm install react-error-boundary
Basic Usage:
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, errorInfo) => {
console.error('Error:', error, errorInfo);
}}
>
<MyComponent />
</ErrorBoundary>
);
}
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div className="error-fallback">
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>
Try Again
</button>
</div>
);
}
withErrorBoundary HOC:
import { withErrorBoundary } from 'react-error-boundary';
class MyComponent extends React.Component {
render() {
return <div>{this.props.children}</div>;
}
}
export default withErrorBoundary(MyComponent);
// Or with custom fallback
export default withErrorBoundary(MyComponent, {
FallbackComponent: CustomFallback,
onError: (error, errorInfo) => {
logError(error, errorInfo);
},
});
ErrorBoundaryType:
import { ErrorBoundary, ErrorBoundaryType } from 'react-error-boundary';
function App() {
return (
<ErrorBoundary
type={ErrorBoundaryType.Component}
FallbackComponent={ComponentFallback}
>
<MyComponent />
</ErrorBoundary>
);
}
function ComponentFallback({ error, resetErrorBoundary }) {
return <div>Component error: {error.message}</div>;
}
7.14 Testing Error Boundaries
Unit Testing:
import { render, screen } from '@testing-library/react';
import ErrorBoundary from './ErrorBoundary';
describe('ErrorBoundary', () => {
it('catches errors and renders fallback', () => {
const ThrowError = () => {
throw new Error('Test error');
};
render(
<ErrorBoundary fallback={<FallbackUI />}>
<ThrowError />
</ErrorBoundary>
);
expect(screen.getByText('Error occurred')).toBeInTheDocument();
});
it('calls onError callback', () => {
const onError = jest.fn();
const ThrowError = () => {
throw new Error('Test error');
};
render(
<ErrorBoundary fallback={<FallbackUI />} onError={onError}>
<ThrowError />
</ErrorBoundary>
);
expect(onError).toHaveBeenCalled();
expect(onError.mock.calls[0][0]).toBeInstanceOf(Error);
});
});
Testing with React Testing Library:
import { render, screen } from '@testing-library/react';
describe('ErrorBoundary Integration', () => {
it('renders fallback when child throws error', () => {
const ThrowComponent = () => {
throw new Error('Test error');
};
render(
<ErrorBoundary fallback={<FallbackUI />}>
<ThrowComponent />
</ErrorBoundary>
);
expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
});
it('renders children when no error', () => {
const SafeComponent = () => <div>Safe content</div>;
render(
<ErrorBoundary fallback={<FallbackUI />}>
<SafeComponent />
</ErrorBoundary>
);
expect(screen.getByText('Safe content')).toBeInTheDocument();
});
});
7.15 Error Boundary Composition
Nested Boundaries:
function App() {
return (
// Global boundary
<ErrorBoundary fallback={<GlobalFallback />}>
<div className="app">
<Header />
<main>
{/* Feature boundaries */}
<ErrorBoundary fallback={<DashboardFallback />}>
<Dashboard />
</ErrorBoundary>
<ErrorBoundary fallback={<ProjectsFallback />}>
<Projects />
</ErrorBoundary>
<ErrorBoundary fallback={<SettingsFallback />}>
<Settings />
</ErrorBoundary>
</main>
<Footer />
</div>
</ErrorBoundary>
);
}
Boundary Hierarchy:
// Outer boundary catches everything
<ErrorBoundary fallback={<AppFallback />}>
<AppLayout>
{/* Inner boundaries for specific features */}
<ErrorBoundary fallback={<DashboardFallback />}>
<Dashboard />
</ErrorBoundary>
<ErrorBoundary fallback={<ProjectsFallback />}>
<Projects />
</ErrorBoundary>
</AppLayout>
</ErrorBoundary>
7.16 Recovering from Errors
State Reset:
class RecoverableErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log error
logError(error, errorInfo);
}
resetError = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
return (
<ErrorFallback
error={this.state.error}
onReset={this.resetError}
/>
);
}
return this.props.children;
}
}
Route Reset:
function ErrorFallback({ error, onReset }) {
const navigate = useNavigate();
const handleReset = () => {
// Reset to home or previous route
navigate('/');
onReset();
};
return (
<div className="error-fallback">
<h2>Something went wrong</h2>
<p>{error?.message}</p>
<button onClick={handleReset}>Go Home</button>
</div>
);
}
Data Refresh:
function RefreshableErrorBoundary({ children, fallback }) {
const [error, setError] = useState(null);
const [retryKey, setRetryKey] = useState(0);
const handleRefresh = () => {
setError(null);
setRetryKey(prev => prev + 1);
};
return (
<ErrorBoundary
fallback={
<div className="error-fallback">
<h2>Error occurred</h2>
<p>{error?.message}</p>
<button onClick={handleRefresh}>Refresh</button>
</div>
}
onError={setError}
>
<React.Fragment key={retryKey}>
{children}
</React.Fragment>
</ErrorBoundary>
);
}
7.17 User-Friendly Error Messages
Contextual Messages:
function ContextualFallback({ error, component }) {
const getErrorMessage = () => {
switch (component) {
case 'Dashboard':
return 'We couldn\'t load your dashboard. Please try again.';
case 'Profile':
return 'We couldn\'t load your profile. Please try again.';
case 'Settings':
return 'We couldn\'t save your settings. Please try again.';
default:
return 'Something went wrong. Please try again.';
}
};
return (
<div className="error-fallback">
<h2>Oops!</h2>
<p>{getErrorMessage()}</p>
{error?.message && (
<details>
<summary>Technical details</summary>
<p>{error.message}</p>
</details>
)}
</div>
);
}
Action-Oriented Messages:
function ActionFallback({ error, onRetry, onGoHome, onContact }) {
return (
<div className="error-fallback">
<div className="error-icon">⚠️</div>
<h2>Something went wrong</h2>
<p>We're sorry for inconvenience.</p>
<div className="error-actions">
<button onClick={onRetry} className="btn-primary">
Try Again
</button>
<button onClick={onGoHome} className="btn-secondary">
Go Home
</button>
<button onClick={onContact} className="btn-tertiary">
Contact Support
</button>
</div>
</div>
);
}
7.18 Common Patterns
Page-Level Boundaries:
function PageWrapper({ children, pageName }) {
return (
<ErrorBoundary
fallback={<PageFallback pageName={pageName} />}
onError={(error) => logPageError(pageName, error)}
>
{children}
</ErrorBoundary>
);
}
// Usage
<PageWrapper pageName="Dashboard">
<Dashboard />
</PageWrapper>
Component-Level Boundaries:
function ComponentWrapper({ children, componentName }) {
return (
<ErrorBoundary
fallback={<ComponentFallback componentName={componentName} />}
onError={(error) => logComponentError(componentName, error)}
>
{children}
</ErrorBoundary>
);
}
// Usage
<ComponentWrapper componentName="UserProfile">
<UserProfile />
</ComponentWrapper>
Feature-Level Boundaries:
function FeatureWrapper({ children, featureName }) {
return (
<ErrorBoundary
fallback={<FeatureFallback featureName={featureName} />}
onError={(error) => logFeatureError(featureName, error)}
>
<Suspense fallback={<FeatureLoading featureName={featureName} />}>
{children}
</Suspense>
</ErrorBoundary>
);
}
// Usage
<FeatureWrapper featureName="Dashboard">
<Dashboard />
</FeatureWrapper>
7.19 Monitoring and Alerting
Error Rate Monitoring:
class ErrorRateMonitor {
constructor() {
this.errors = [];
this.startTime = Date.now();
this.threshold = 10; // 10 errors per minute
}
recordError(error) {
const now = Date.now();
this.errors.push({ error, timestamp: now });
// Clean old errors (older than 1 minute)
this.errors = this.errors.filter(e => now - e.timestamp < 60000);
// Check threshold
if (this.errors.length >= this.threshold) {
this.alertHighErrorRate();
}
}
alertHighErrorRate() {
console.error('High error rate detected!');
// Send alert to monitoring service
sendAlert('High error rate', {
count: this.errors.length,
duration: Date.now() - this.startTime,
});
}
}
const monitor = new ErrorRateMonitor();
class MonitoredErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
monitor.recordError(error);
logError(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <FallbackUI />;
}
return this.props.children;
}
}
8. Related Skills
02-frontend/react-best-practices02-frontend/nextjs-patterns14-monitoring-observability/error-tracking01-foundations/code-review