React 78 ⚛️ Catching Rendering Errors with Error Boundaries
React’s rendering process is a synchronous, recursive operation. When a component throws during render, React cannot simply catch the error and continue—the component tree may be in an inconsistent state, with some components mounted and others not, some hooks already called and others not. In React 15 and earlier, this inconsistency would corrupt React’s internal state and produce cryptic errors on subsequent renders . Error boundaries were introduced in React 16 to solve this: they are the only mechanism that can catch a rendering error, log it, and display a fallback UI instead of letting the entire application crash .
The mechanism is precise and deliberately limited. An error boundary is a class component that implements either static getDerivedStateFromError() or componentDidCatch(). When a descendant throws during render, lifecycle method, or constructor, React finds the nearest error boundary above the crashing component, calls its getDerivedStateFromError to update state, then calls componentDidCatch for logging, and renders the fallback instead of the crashed subtree . The boundary does not catch errors in event handlers, asynchronous callbacks, server-side rendering, or its own render method—those are outside the rendering phase that the boundary can intercept .
Key point: Error boundaries are class components that implement static getDerivedStateFromError() to render a fallback and componentDidCatch() to log the error; they catch errors during rendering, in lifecycle methods, and in constructors of their descendant tree, but they do not catch errors in event handlers, async callbacks, server rendering, or the boundary itself.
Why error boundaries are limited to rendering errors
The rendering phase problem. React renders components synchronously. When render() throws, React is mid-traversal—some components have been rendered, some have not, and hooks may have been partially called. There is no safe point to resume. The only correct response is to discard the partially-rendered subtree and render something else in its place . This is what the boundary does: it catches the error, sets hasError to true, and re-renders its fallback instead of its children.
The event handler problem. An event handler runs after rendering is complete. When onClick throws, React has already committed the UI and knows exactly what is on screen. There is no inconsistent tree to recover from. React does not need a boundary to recover, and the boundary does not fire . The correct tool for event handler errors is a regular try/catch inside the handler itself, which can set local state and show an inline error message without unmounting the component .
The async problem. A promise rejection or a setTimeout callback runs outside React’s rendering cycle. React has no way to associate the error with a component, and the boundary has no mechanism to catch it. The workaround is to bridge the error into the rendering phase: catch it in the async callback, call a state setter that triggers a re-render, and throw the error during that render so the boundary catches it . Libraries like react-error-boundary provide a useErrorBoundary hook that does exactly this: showBoundary(error) schedules a state update that throws during render, which the boundary then catches .
The boundary-self problem. An error boundary cannot catch its own errors. If the fallback UI itself throws, the error propagates to the nearest boundary above it in the tree . This is the same behavior as a JavaScript catch block: a catch cannot catch an error thrown inside itself.
a. Creating an error boundary with getDerivedStateFromError and componentDidCatch
The class component has two lifecycle methods that together make it a boundary. static getDerivedStateFromError(error) is called during the render phase, receives the error, and returns the state update that switches to the fallback. It must be pure—no side effects, no logging, just the state update . componentDidCatch(error, errorInfo) is called after the render phase, receives the error and a componentStack string identifying where the error occurred, and is where logging to an external service belongs .
import { Component } from 'react'
class ErrorBoundary extends Component {
// Initial state: no error, render children normally
state = { hasError: false }
// Called during render when a descendant throws
// must be pure; returns the state update
static getDerivedStateFromError(error) {
// Switch to the fallback UI on the next render
return { hasError: true }
}
// Called after the error has been committed
// safe place for side effects: logging, reporting
componentDidCatch(error, errorInfo) {
// error is the thrown Error
// errorInfo.componentStack identifies the component path
logErrorToService(error, errorInfo.componentStack)
}
render() {
// If an error was caught, render the fallback
if (this.state.hasError) {
// fallback can be a prop, a default UI, or a render function
return this.props.fallback ?? <h1>Something went wrong.</h1>
}
// Otherwise render the children normally
return this.props.children
}
}
The componentStack is the property that makes the boundary useful for debugging. It shows the path through the component tree that led to the crash, which is often more actionable than the JavaScript stack trace because it identifies which component in the tree was responsible . In development, React also prints the component stack to the console for every error, even if the boundary swallows it .
b. Placement: application, route, and component boundaries
The placement of boundaries determines the blast radius of a crash. A single boundary at the root means any error anywhere unmounts the entire application and shows a full-page fallback. A boundary around every component means excessive boilerplate and a fragmented error experience. The correct approach is to place boundaries at meaningful UI sections—units that make sense to fail as a group .
Application-level boundary. Wraps the root. It catches catastrophic failures and shows a full-page fallback with a reload option. This is the last line of defence, not the primary error-handling strategy .
Route-level boundary. Wraps each route’s content. A crash in /dashboard shows a fallback in the dashboard area, while the navigation and the rest of the shell remain interactive. The user can navigate to /settings without reloading the page . The resetKeys prop is the mechanism that makes this work: when the route path changes, the boundary resets and re-renders its children, clearing the error state .
Component-level boundary. Wraps independent widgets: a chart, a feed, a sidebar. A crash in the chart shows a small inline fallback in the chart’s area, and the rest of the page continues working . This is the granularity that prevents one broken feature from taking down the entire view.
function Dashboard() {
return (
<div className="layout">
{/* Sidebar error does not crash the main content */}
<ErrorBoundary fallback={<SidebarFallback />}>
<Sidebar />
</ErrorBoundary>
{/* Chart error does not crash the feed */}
<main>
<ErrorBoundary fallback={<ChartFallback />}>
<RevenueChart />
</ErrorBoundary>
<ErrorBoundary fallback={<FeedFallback />}>
<ActivityFeed />
</ErrorBoundary>
</main>
</div>
)
}
c. Recovery: resetting the boundary and retrying
A boundary that shows a fallback without a recovery path is a dead end. The user sees “Something went wrong” and has no way to try again. The recovery mechanism is to reset the boundary’s state, which re-renders the children and gives them another chance .
The react-error-boundary library provides a clean API for this. Its ErrorBoundary component accepts a FallbackComponent that receives error and resetErrorBoundary. Calling resetErrorBoundary() sets hasError back to false, and React re-renders the children . The resetKeys prop automates the reset: when any key in the array changes, the boundary resets. This is ideal for route parameters, search queries, and any state that identifies a different data context .
import { ErrorBoundary } from 'react-error-boundary'
// The fallback receives the error and the reset function
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
{/* Clicking this resets the boundary and retries the render */}
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)
}
function UserProfile({ userId }) {
return (
// resetKeys: when userId changes, the boundary auto-resets
// this is correct because a different user is a different context
<ErrorBoundary
FallbackComponent={ErrorFallback}
resetKeys={[userId]}
onReset={() => {
// optional: clear any state that caused the error
}}
>
<ProfileDetails userId={userId} />
</ErrorBoundary>
)
}
The onReset callback runs when the boundary resets, which is the place to clear any application state that contributed to the error—a failed query cache, a stale filter, a corrupted local state . Without it, the retry may immediately recreate the same error.
For asynchronous errors that originate outside the rendering phase, the useErrorBoundary hook provides the bridge. It returns a showBoundary function that schedules a state update which throws during the next render, allowing the boundary to catch an error that occurred in a promise rejection or a callback .
import { useErrorBoundary } from 'react-error-boundary'
import { useEffect } from 'react'
function DataLoader({ url }) {
// showBoundary re-throws an async error during render
// so the nearest ErrorBoundary can catch it
const { showBoundary } = useErrorBoundary()
useEffect(() => {
fetch(url)
.then((res) => res.json())
// catch the rejection and route it to the boundary
.catch(showBoundary)
}, [url, showBoundary])
return <div>Loading...</div>
}
Complete Example Session
The following session builds a dashboard with an application-level boundary, a route-level boundary that resets on navigation, and a component-level boundary that isolates a fragile chart.
// ============================================
// PART 1: THE BASE ERROR BOUNDARY
// ============================================
import { Component } from 'react'
class ErrorBoundary extends Component {
// Track whether a descendant has thrown
state = { hasError: false }
// Called during render; must be pure
static getDerivedStateFromError() {
// Switch to fallback on the next render
return { hasError: true }
}
// Called after commit; safe for logging
componentDidCatch(error, errorInfo) {
// Report the error and the component stack
logToService(error, errorInfo.componentStack)
}
render() {
// Render the fallback if an error was caught
if (this.state.hasError) {
// Allow the parent to provide a custom fallback
return this.props.fallback
}
// Otherwise render the children
return this.props.children
}
}
// ============================================
// PART 2: A COMPONENT THAT THROWS
// ============================================
function FragileChart({ data }) {
// Simulate a render error when data is missing
if (!data) {
throw new Error('Chart data is missing')
}
return <div>Chart with {data.length} points</div>
}
// ============================================
// PART 3: THE DASHBOARD WITH BOUNDARIES
// ============================================
function Dashboard({ route, chartData }) {
return (
<div>
{/* Application-level fallback for catastrophic errors */}
<ErrorBoundary fallback={<h1>Application error. Reload.</h1>}>
{/* Route content is isolated */}
<ErrorBoundary fallback={<p>This page failed to load.</p>}>
<RouteContent route={route} />
</ErrorBoundary>
{/* The chart is isolated so its failure stays local */}
<ErrorBoundary fallback={<p>Chart unavailable.</p>}>
<FragileChart data={chartData} />
</ErrorBoundary>
</ErrorBoundary>
</div>
)
}
The three parts cover the base boundary with both lifecycle methods, a component that throws during render, and the nested placement that isolates the chart from the route content and the application shell. What the session demonstrates is that a rendering error in FragileChart unmounts only the chart’s subtree—the route content and the navigation remain interactive—and the error is logged with its component stack.
Quick Reference
Error Boundary API
| Method | Phase | Purpose |
|---|---|---|
static getDerivedStateFromError(error) | Render | Update state to show fallback |
componentDidCatch(error, info) | Commit | Log error and component stack |
What Boundaries Catch
| Source | Caught? |
|---|---|
| Render method | Yes |
| Lifecycle methods | Yes |
| Constructors | Yes |
| Event handlers | No |
setTimeout / async callbacks | No |
| Server-side rendering | No |
| The boundary itself | No |
Placement Strategy
| Level | Scope | Use Case |
|---|---|---|
| Application | Entire app | Last resort, full-page fallback |
| Route | Per route | Isolate page crashes, reset on navigation |
| Component | Widget / section | Isolate charts, feeds, sidebars |
Recovery Mechanisms
| Mechanism | API | Effect |
|---|---|---|
| Manual reset | resetErrorBoundary() | Re-render children |
| Auto reset | resetKeys={[dep]} | Reset when dep changes |
| Async bridge | showBoundary(error) | Route async error to boundary |
Best Practices
✅ Do This:
// Use getDerivedStateFromError for state, componentDidCatch for logging
static getDerivedStateFromError() { return { hasError: true } }
componentDidCatch(error, info) { log(error, info.componentStack) }
// Place boundaries at meaningful sections
<ErrorBoundary fallback={<ChartFallback />}><Chart /></ErrorBoundary>
// Use resetKeys for context changes
<ErrorBoundary resetKeys={[userId]} FallbackComponent={Fallback}>
// Bridge async errors with showBoundary
fetchData().catch(showBoundary)
❌ Don’t Do This:
// Use try/catch around JSX — it won't catch render errors
try { return <Child /> } catch (e) {} // useless
// Use an error boundary for event handler errors
onClick={() => { throw new Error() }} // boundary won't catch
// Show stack traces in production
<pre>{error.stack}</pre> // leak internal paths
// Place a boundary around every component
<ErrorBoundary><Button /></ErrorBoundary> // excessive
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Boundary does not catch event error | Events are outside render phase | Use try/catch in handler |
| Boundary does not catch async error | Promises are outside render phase | Bridge with showBoundary |
| Boundary does not catch its own error | Boundary only catches descendants | Add a parent boundary |
| Blank screen after error | No boundary at the root | Add application-level boundary |
| Retry recreates the error | State that caused it is not reset | Clear state in onReset |
try/catch around JSX | Confused with rendering | Use a boundary |
Real-World Examples
1. Basic boundary
class ErrorBoundary extends Component {
state = { hasError: false }
static getDerivedStateFromError() { return { hasError: true } }
render() { return this.state.hasError ? <Fallback /> : this.props.children }
}
2. Boundary with logging
componentDidCatch(error, info) { logToSentry(error, info.componentStack) }
3. Fallback with retry
<ErrorBoundary fallbackRender={({ error, reset }) => (
<button onClick={reset}>Retry</button>
)}>
4. Route boundary with reset key
<ErrorBoundary resetKeys={[location.pathname]} FallbackComponent={RouteFallback}>
5. Widget isolation
<ErrorBoundary fallback={<p>Widget failed</p>}><Widget /></ErrorBoundary>
6. Async bridge
const { showBoundary } = useErrorBoundary()
useEffect(() => { fetch().catch(showBoundary) }, [])
7. React 19 transition error
startTransition(() => { setPage(path) }) // errors bubble to boundary
8. Development-only stack
{import.meta.env.DEV && <pre>{error.stack}</pre>}
9. Nested boundaries
<ErrorBoundary><ErrorBoundary><Chart /></ErrorBoundary></ErrorBoundary>
10. Test a boundary
render(<ErrorBoundary fallback={<p>Error</p>}><Thrower /></ErrorBoundary>)
expect(screen.getByText('Error')).toBeInTheDocument()
Visual
What the Boundary Catches
┌──────────────────────────────────────────────┐
│ ErrorBoundary │
│ ┌────────────────────────────────────────┐ │
│ │ render() ✅ caught │ │
│ │ constructor ✅ caught │ │
│ │ lifecycle ✅ caught │ │
│ │ onClick ❌ not caught │ │
│ │ setTimeout ❌ not caught │ │
│ │ fetch().catch ❌ not caught │ │
│ └────────────────────────────────────────┘ │
│ │
│ The boundary only sees errors in its │
│ descendants' rendering phase. │
└──────────────────────────────────────────────┘
Boundary Lifecycle
┌──────────────────────────────────────────────┐
│ Descendant throws during render │
│ │ │
│ ▼ │
│ React finds nearest ErrorBoundary │
│ │ │
│ ▼ │
│ getDerivedStateFromError(error) │
│ → returns { hasError: true } │
│ │ │
│ ▼ │
│ componentDidCatch(error, info) │
│ → logs to service │
│ │ │
│ ▼ │
│ render() returns fallback │
└──────────────────────────────────────────────┘
Placement Granularity
┌──────────────────────────────────────────────┐
│ APPLICATION BOUNDARY (last resort) │
│ ┌────────────────────────────────────────┐ │
│ │ ROUTE BOUNDARY │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ WIDGET BOUNDARY │ │ │
│ │ │ Chart crashes → only chart area │ │ │
│ │ │ shows fallback │ │ │
│ │ └──────────────────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
Async Error Bridge
┌──────────────────────────────────────────────┐
│ fetch() rejects │
│ │ │
│ ▼ │
│ .catch(showBoundary) │
│ │ │
│ ▼ │
│ showBoundary sets state that throws │
│ │ │
│ ▼ │
│ During render, error is thrown │
│ │ │
│ ▼ │
│ ErrorBoundary catches it │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Introduced | React 16 |
| Implementation | Class component |
| State method | static getDerivedStateFromError |
| Logging method | componentDidCatch(error, info) |
| Catches | Render, lifecycle, constructor |
| Does not catch | Events, async, SSR, self |
| Uncaught error | Unmounts entire tree |
| Recovery | Reset state or resetKeys |
| Library | react-error-boundary |
Key takeaways:
- Error boundaries are the only mechanism that catches rendering errors.
try/catcharound JSX does not work because the error is thrown during React’s render traversal, not during the function call that returns the JSX . - The boundary has two methods with distinct roles.
getDerivedStateFromErrorupdates state during render and must be pure.componentDidCatchruns after commit and is where logging belongs . - Boundaries do not catch event handler errors. Events run after rendering, when React already knows the UI is consistent. Use
try/catchinside the handler for those . - Boundaries do not catch async errors. A promise rejection or
setTimeoutcallback is outside the render phase. Bridge it withshowBoundaryor a state update that re-throws during render . - Placement determines the blast radius. An application-level boundary is the last resort. Route-level boundaries isolate pages. Widget-level boundaries isolate charts and feeds so a single failure does not take down the rest of the page .
- A retry needs a reset. The boundary’s state must be cleared, and any application state that caused the error must be cleared with it.
resetKeysautomates this for context changes like route parameters . - Uncaught rendering errors unmount the entire tree. React made this choice deliberately: a corrupted UI is worse than no UI. The boundary is what prevents the corruption from reaching the user .
Remember: An error boundary is not a try/catch for components. It is a rendering-phase safety net. It catches the errors that occur while React is building the tree—in render, in lifecycle methods, in constructors—and replaces the crashed subtree with a fallback. It does not catch the errors that occur after the tree is built: event handlers, timers, promises. Those require their own handling, and the bridge to route them into the boundary is a state update that throws during render. The placement of the boundary is a design decision: too high, and one error takes down the whole app; too low, and the boilerplate outweighs the benefit. The right granularity is the UI section that makes sense to fail as a unit—the route, the widget, the panel—with an application-level boundary as the final safety net.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!