React: Error Boundaries and Debugging
Last updated: 2026-08-26
Tom was responsible for the e-commerce back-end, which experienced a "white screen" issue on the live site—users reported that the order details page was completely blank when opened, and the console displayed the erro
Cannot read properties of indefinido (reading 'name'). The cause was an uncaught JavaScript erro thrown by a child component during the rendering phase, which caused the entire React component tree to crash. Tom needs to establish an erro-boundary system within the application to ensure that the failure of individual components does not affect the overall page usability.
1. What You'll Learn
- The Principles and Best Practices of Error Boundaries
- How to Analyze Flame Charts in React DevTools Profiler
- The Profiler component measures the rendering time of components
- Diagnosis and Resolution Strategies for Common Performance Issues
2. Conceptual Diagrams
The figure below illustrates the Error Boundary capture process and the Profiler's analysis chain:
flowchart TD
A[React Component tree] --> B[Error Boundary]
B --> C{Is there an error in the rendering of the child component??}
C -->|No| D[Normal Rendering]
C -->|is | E[getDerivedStateFromError]
E --> F[Update state.hasError = true]
F --> G[Rendering fallback UI]
G --> H{Click to retry??}
H -->|is | I[Reset state]
I --> A
H -->|No| J[Stay in the lower division UI]
K[React DevTools Profiler] --> L[Recording Component Rendering]
L --> M[Flame Pattern Analysis]
M --> N["Identify Time-Consuming Components(Yellow/Red)"]
N --> O[React.memo / useMemo Optimization]
style B fill:#e3f2fd,stroke:#1565c0
style E fill:#fff3e0,stroke:#e65100
style G fill:#e8f5e9,stroke:#2e7d32
style K fill:#f3e5f5,stroke:#7b1fa2
3. A Real-Life Scenario
| Error Handling Level | Scope | Recovery Strategy | Applicable Scenarios |
|---|---|---|---|
| try/catch | Single asynchronous operation | Retry/Fallback | API requests, Promise operations |
| Error Boundary | Child Component tree Rendering Error | Fallback UI + Retry Button | Component White Screen Protection |
| Global unhandledrejection | Uncaught Promise errors | Log reporting | Catch-all monitoring |
| window.onerror | Global Synchronization Errors | Log Reporting | Catch-All Monitoring |
| React DevTools | Debugging During Development | Troubleshooting | Troubleshooting Performance/Rendering Issues |
Tom's order details page consists of the following components:
OrderPage
├── OrderHeader (Order Number、Status)
├── OrderItems (Product List)
│ └── OrderItem × N(Single Item,Includes price calculation)
├── ShippingInfo (Shipping Information)
└── PaymentInfo (Payment Information)
The cause of the online incident was that the price field was missing from the product data for a particular order, and when the OrderItem component accessed item.price.toFixed(2), it threw a TypeError error, causing the entire OrderPage to display a blank screen.
The correct approach is to wrap the OrderItems area with an Error Boundary so that even if the product list fails to render, the order header and payment information will still display properly. Additionally, Tom needs to learn how to use the React DevTools Profiler to identify performance bottlenecks.
(1) Error Boundary — Component-Level Safety Net
An Error Boundary is a declarative error-handling mechanism provided by React. When any component in a subtree throws an error during the rendering phase, in a lifecycle method, or in its constructor, the Error Boundary can catch that error and display a fallback UI, rather than causing the entire application to display a blank screen.
Note: Error Boundary can currently only be implemented using class components (React plans to provide a Hook version in future releases).
▶ Example 1: Generic ErrorBoundary Component
Output:
Error boundary catches rendering errors, shows fallback UI
import { Component, ErrorInfo, ReactNode } from 'react'
interface ErrorBoundaryProps {
children: ReactNode
/** Custom Downgrade UI */
fallback?: ReactNode
/** Error Callback(Submit Sentry etc.) */
onError?: (error: Error, errorInfo: ErrorInfo) => void
}
interface ErrorBoundaryState {
hasError: boolean
error: Error | null
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { hasError: false, error: null }
}
// Static Methods:Update Based on the Error state
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error }
}
// Life Cycle:Performing side effects after catching an error(Log Reporting)
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary An error was caught:', error.message)
console.error('Component Stack:', errorInfo.componentStack)
// Reported to the error monitoring service
if (this.props.onError) {
this.props.onError(error, errorInfo)
}
// Can be integrated into actual projects Sentry:
// Sentry.captureException(error, { extra: errorInfo })
}
handleReset = () => {
this.setState({ hasError: false, error: null })
}
render() {
if (this.state.hasError) {
// Use Custom fallback Or default to a lower level UI
if (this.props.fallback) {
return this.props.fallback
}
return (
<div
role="alert"
style={{
padding: '32px 24px',
margin: 16,
background: '#fff2f0',
border: '1px solid #ffccc7',
borderRadius: 8,
textAlign: 'center',
}}
>
<h2 style={{ color: '#ff4d4f', margin: '0 0 12px' }}>
A component error occurred
</h2>
<p style={{ color: '#666', marginBottom: 8, fontSize: 14 }}>
{this.state.error?.message || 'An unknown error has occurred'}
</p>
<button
onClick={this.handleReset}
style={{
padding: '6px 20px',
background: '#ff4d4f',
color: '#fff',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
fontSize: 14,
}}
>
Retry
</button>
</div>
)
}
return this.props.children
}
}
export default ErrorBoundary
Output:
TypeScript props: interface ButtonProps { text: string; onClick: () => void; color?: string }. Editor auto-completes, compile-time errors on misuse.
Using ErrorBoundary in Real-World Projects
// Layered Wrapping — Each independent area has its own ErrorBoundary
function OrderPage({ orderId }: { orderId: string }) {
return (
<div>
{/* Order Header Information:It will display correctly even if there is an error below. */}
<ErrorBoundary fallback={<p>Failed to load order</p>}>
<OrderHeader orderId={orderId} />
</ErrorBoundary>
{/* Product List:Errors in one area do not affect other areas */}
<ErrorBoundary
onError={(err) => {
// Rendering Error in the List of Submitted Products
fetch('/api/log-error', {
method: 'POST',
body: JSON.stringify({ error: err.message, orderId }),
})
}}
>
<OrderItems orderId={orderId} />
</ErrorBoundary>
{/* Payment Information */}
<ErrorBoundary>
<PaymentInfo orderId={orderId} />
</ErrorBoundary>
</div>
)
}
▶ Example 2: The UserProfile component with error recovery
Output:
ErrorBoundary catches render errors → shows fallback UI "Something went wrong" + "Retry" button. Child components crash gracefully.
In real-world scenarios, sometimes displaying a fallback UI alone isn’t enough—users may need to refresh specific data. Here’s an example of how to use an error boundary with a “Retry” feature:
import { useState } from 'react'
import ErrorBoundary from './ErrorBoundary'
// Simulating Data Retrieval That Results in Errors
function fetchUserData(userId: number) {
return fetch(`/api/users/${userId}`).then(res => {
if (!res.ok) throw new Error('Failed to retrieve user data')
return res.json()
})
}
// Data display components that may have rendering errors
function UserInfo({ userId }: { userId: number }) {
const [user, setUser] = useState<any>(null)
const [loading, setLoading] = useState(true)
useState(() => {
fetchUserData(userId)
.then(setUser)
.finally(() => setLoading(false))
})
if (loading) return <p>Loading......</p>
// If user Data Structure Exception,An error may occur here
return (
<div>
<h3>{user.name}</h3> {/* possibly:Cannot read properties of undefined */}
<p>{user.profile.bio}</p> {/* possibly:Cannot read properties of undefined */}
</div>
)
}
// Outer Container:With retries key Mechanism
function UserProfile({ userId }: { userId: number }) {
const [retryKey, setRetryKey] = useState(0)
return (
<ErrorBoundary
key={retryKey} // Change key It will unmount and remount the subtree
fallback={
<div style={{ padding: 24, textAlign: 'center' }}>
<p>Error loading user information</p>
<button onClick={() => setRetryKey(k => k + 1)}>
Retry Loading
</button>
</div>
}
>
<UserInfo userId={userId} />
</ErrorBoundary>
)
}
Key Tip: key={retryKey} Have the ErrorBoundary unmount and recreate its subtree when a retry is triggered, thereby resetting the state of all child components.
(2) Errors That Cannot Be Caught by Error Boundaries
Error Boundaries are not a panacea; they cannot catch the following four types of errors:
| Error Type | Cause | Solution |
|---|---|---|
| Errors in Event Handling | Event Handlers Do Not Execute During the Rendering Phase | Enclose Event Handling Logic in a try/catch Block |
| Errors in Asynchronous Code | setTimeout / Promise callbacks do not occur within the React rendering cycle | Use try/catch or Promise.catch |
| Errors in Server-Side Rendering (SSR) | Error Boundaries Only Take Effect on the Client Side | Wrap Rendering in try/catch Blocks for SSR |
| Error Boundary's own error | It throws an error that cannot be caught by itself | Wrap it in another Error Boundary at the outermost level |
Event Handling + Proper Error Handling in Asynchronous Code
function PaymentForm() {
async function handleSubmit() {
try {
const result = await submitPayment()
// Processed successfully
} catch (error) {
// Asynchronous errors are caught here,Error Boundary That's none of my business
console.error('Payment Failed:', error)
// Display Error UI(For example, setting state)
setError(error instanceof Error ? error.message : 'Payment Failed')
}
}
// Errors in the event must also be used try/catch
function handleClick() {
try {
processPayment()
} catch (error) {
setError('Processing Failed,Please try again.')
}
}
}
(3) Analyzing Performance Using the React DevTools Profiler
The Profiler tab in React DevTools is a core tool for analyzing component rendering performance. It generates a "flame graph" that visually illustrates the rendering time for each component.
Instructions for Use
1. Open your browser DevTools → Components Tabs
2. Switch to Profiler Sublabel
3. Click the blue record button(Start Recording)
4. Performing actions on the page(Click、Scrolling, etc.)
5. Click the Stop button(End Recording)
6. View the flame diagram
How to Interpret Flame Charts
┌────────────────────────────────────────────┐
│ App (0.3ms) │
│ ├── Navbar (0.2ms) │
│ ├── OrderPage (2.1ms) │
│ │ ├── OrderHeader (0.4ms) ── Gray │
│ │ ├── OrderItems (1.5ms) ── Yellow │
│ │ │ └── OrderItem × 20 (each 0.3ms) │
│ │ └── PaymentInfo (0.2ms) ── Gray │
│ └── Footer (0.1ms) │
└────────────────────────────────────────────┘
- Gray: No re-rendering (ideal scenario)
- Blue: Rendered again, but the processing time was normal
- Yellow/Red: Rendering takes a long time; requires attention
▶ Example 3: Measuring Rendering Time with the Profiler Component
Output:
Profiler logs render timings: mount 45ms, update 12ms. Identify slow components and optimize with memo/callback
React's built-in <Profiler> component can accurately measure the rendering time of a specific component in your code, making it suitable for automated monitoring of performance metrics:
import { Profiler } from 'react'
type ProfilerPhase = 'mount' | 'update' | 'nested-update'
interface ProfileMetrics {
id: string
phase: ProfilerPhase
actualDuration: number // Actual rendering time for this render(milliseconds)
baseDuration: number // Worst-case runtime for a subtree
startTime: number // Render Start Timestamp
commitTime: number // Submit to DOM timestamp
interactions: Set<any> // Related Interaction Tracking
}
// Performance Monitoring Callbacks
function onRenderCallback(
id: string,
phase: ProfilerPhase,
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number,
) {
// Record to the performance log
if (actualDuration > 16) { // More than 16ms = Frame drop threshold (60fps)
console.warn(
`[Performance Alerts] ${id} in ${phase} Time Taken per Stage ${actualDuration.toFixed(1)}ms,` +
`More than 16ms Frame Budget!`
)
// Reported to the performance monitoring system
// reportPerformance({ id, phase, actualDuration, baseDuration })
}
// Output from the development environment to the console
if (process.env.NODE_ENV === 'development') {
console.table({
'Components': id,
'Phase': phase,
'Actual time taken(ms)': actualDuration.toFixed(1),
'Benchmark Duration(ms)': baseDuration.toFixed(1),
})
}
}
// Big Data List——Potential Performance Bottlenecks
function ProductList({ products }: { products: Product[] }) {
return (
<Profiler id="ProductList" onRender={onRenderCallback}>
<div style={{ display: 'grid', gap: 16, gridTemplateColumns: 'repeat(3, 1fr)' }}>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</Profiler>
)
}
Common Performance Optimization Strategies
// 1. React.memo — Avoid Unnecessary Re-rendering
const ProductCard = React.memo(function ProductCard({
product,
}: {
product: Product
}) {
return (
<div style={{ border: '1px solid #eee', padding: 16, borderRadius: 8 }}>
<img src={product.image} alt={product.name} width="100%" />
<h4>{product.name}</h4>
<p>${product.price}</p>
</div>
)
})
// 2. useMemo — Cache the results of expensive computations
function OrderSummary({ items }: { items: OrderItem[] }) {
const totalPrice = useMemo(() => {
return items.reduce((sum, item) => {
// Assuming that complex currency conversions were performed here
return sum + convertCurrency(item.price, item.currency)
}, 0)
}, [items])
return <p>Total:${totalPrice.toFixed(2)}</p>
}
// 3. useCallback — Stable function references
function OrderList({ orders, onSelect }: {
orders: Order[]
onSelect: (id: string) => void
}) {
// ✅ use useCallback Keep references consistent
const handleSelect = useCallback((id: string) => {
onSelect(id)
}, [onSelect])
return orders.map(order => (
<OrderRow key={order.id} order={order} onSelect={handleSelect} />
))
}
(4) Debugging Using the React DevTools Components Panel
In addition to the Profiler, the Components panel in React DevTools is also a powerful tool for everyday debugging:
| Feature | Purpose | Operation |
|---|---|---|
| Browse the component tree | View the component hierarchy | Click DevTools → Components |
| View Props/State in Real Time | Check the component's current state | Select a component to view the right-hand panel |
| Directly modify the state | Test the UI in different states | Double-click a state value to edit it directly |
| Search Component | Quick Locate Component | Ctrl+F Enter component name |
| Go to source code | View component implementation | Click the <> icon |
// DevTools Components Panel Examples
<OrderPage>
<ErrorBoundary>
<OrderHeader
orderNumber="ORD-2026-0001" ← Props Real-time Display
status="shipped" ← Can be edited directly during testing
/>
</ErrorBoundary>
<ErrorBoundary>
<OrderItems>
<OrderItem product={...} /> ← State Expand to view
<OrderItem product={...} />
</OrderItems>
</ErrorBoundary>
</OrderPage>
▶ Example 4: Handling API Request Errors—Retrieving Data with Retry Mechanisms
Output:
Async data fetching with loading/error/success states
function useFetchWithRetry(url, maxRetries = 3) {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const [loading, setLoading] = useState(true)
const [retries, setRetries] = useState(0)
const fetchData = useCallback(async () => {
setLoading(true)
setError(null)
try {
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const json = await res.json()
setData(json)
} catch (err) {
if (retries < maxRetries) {
setRetries(r => r + 1)
setTimeout(fetchData, 1000 * (retries + 1))
} else {
setError(err.message)
}
} finally {
setLoading(false)
}
}, [url, retries, maxRetries])
useEffect(() => { fetchData() }, [url])
return { data, error, loading, retries, refetch: () => { setRetries(0); fetchData() } }
}
function UserList() {
const { data: users, error, loading, retries, refetch } = useFetchWithRetry('/api/users')
if (loading) return <p>Loading... {retries > 0 && `(retry ${retries})`}</p>
if (error) return (
<div style={{ padding: 20, textAlign: 'center' }}>
<p style={{ color: '#ff4d4f' }}>Error: {error}</p>
<button onClick={refetch} style={{ padding: '8px 16px', cursor: 'pointer' }}>Retry</button>
</div>
)
return (
<ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
)
}
Output:
Custom hook returns { data, loading, error }. Usage: const { data, loading } = useFetch("/api/users"). Auto-fetches on mount.
▶ Example 5: Global Error Monitoring—Sentry Integration
Output:
Uses useeffect
// lib/errorReporting.ts
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN
function initErrorReporting() {
if (typeof window === 'undefined') return
if (!SENTRY_DSN) return
// Sentry.init({ dsn: SENTRY_DSN, ... })
// Simplified Example:Simulation Using Global Event Listeners
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled Promise:', event.reason)
reportError({
type: 'unhandledrejection',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
timestamp: new Date().toISOString(),
})
})
window.addEventListener('error', (event) => {
console.error('Global Error:', event.error)
reportError({
type: 'window.error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
timestamp: new Date().toISOString(),
})
})
}
function reportError(payload) {
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}).catch(() => {})
}
// app/layout.tsx
function RootLayout({ children }) {
useEffect(() => { initErrorReporting() }, [])
return <html><body>{children}</body></html>
}
Output:
fetch("/api/users") → response.json() → setUsers(data). Basic GET request on component mount.
❓ FAQ
try/catch), errors in asynchronous code (use Promise.catch), errors during server-side rendering, and errors within the Error Boundary itself. When designing error handling, you need to combine try/catch and Error Boundaries in a layered approach.React.memo automatically optimize all components?useMemo / useCallback to keep the references stable, or pass a second argument—a custom comparison function React.memo(Comp, (prev, next) => deepEqual(prev, next))—to React.memo.<Profiler> component affect performance in a production environment?<Profiler> component incurs a slight performance overhead in a production environment. It is recommended to use it only in development environments or to control it via environment variables: {process.env.NODE_ENV === 'development' && <Profiler>...}. When performance monitoring is required in production, consider using dedicated performance monitoring libraries (such as web-vitals) or Sentry’s performance tracing features.getDerivedStateFromError and componentDidCatch, which are only supported by class components. The React team has indicated that a Hook version may be available in the future, but for now (React 18/19), they can only be implemented using class components. You can create a class-based Error Boundary component and then wrap it in a function component to handle the error recovery logic.📖 Summary
- Error Boundary is React's declarative error-handling solution, which prevents a single crash from causing the entire app to display a blank screen.
- Error Boundary can only be implemented using class components, through the collaboration of the
getDerivedStateFromErrorandcomponentDidCatchlifecycle methods. - Changing the
keyin the Error Boundary allows you to reset (remount) the subtree. - Error Boundary cannot catch errors in event handlers, asynchronous code, SSR, or within itself
- The React DevTools Profiler displays rendering times using a flame graph; yellow and red components are the ones that need optimization.
- React.memo, useMemo, and useCallback are the "trinity" of React performance optimization
📝 Exercises
- Create an
ErrorBoundarycomponent and use it hierarchically withinOrderPage: WrapOrderHeader,OrderItems, andPaymentInfoin separateErrorBoundarycomponents. Manually trigger a rendering error (such as passing incorrect props) to verify that only the affected area displays the fallback UI, while the rest of the interface renders normally. - Use the
Profilercomponent to measure the rendering time of a component containing 100 list items. After optimizing withReact.memo, measure the rendering time again and compare the difference inactualDurationbetween the two measurements to verify the effectiveness of the optimization. - Open the React DevTools Profiler in Chrome, record a page interaction (such as a search, filter, or sort), identify the component that takes the longest to render in the flame graph, analyze the cause, and optimize it using
useMemo/useCallback.