Kazi Rahamatullah
Kazi Rahamatullah
AboutProjectsBlogContact
UsesBooks
ResumeView CV
Kazi Rahamatullah

© Copyright 2026 Kazi Rahamatullah

AboutProjectsBlogBooksUses
Twitter/XGitHubProduct HuntCodeSandbox
Back to Blog
ReactDesign PatternsError BoundariesResilienceError Handling

Error Boundary Pattern in React

Master React Error Boundaries — catch render crashes, fallback UI, class component requirements, granular placement, retry with key reset, logging, and what boundaries cannot catch.

Jul 5, 202611 min read

Introduction

The Error Boundary pattern catches JavaScript errors during rendering in a child subtree and replaces the crashed UI with a fallback — instead of unmounting the entire app to a blank screen. Boundaries are React's structured recovery mechanism for component-tree failures.

This guide covers:

  1. Definition & purpose — catch, replace, fallback, prevent blank screens
  2. Default React behavior — how errors bubble without boundaries
  3. Implementation requirements — class components, lifecycle methods, state, props
  4. Application strategies — granular placement, sibling component rule
  5. Retry logic pattern — retryKey + key prop remount
  6. Debugging & logging — error message, component stack, monitoring
  7. Limits & best practices — what boundaries do not catch

Note

Error boundaries handle render-phase failures in the tree below them. They do not replace try/catch in event handlers, async code, or Next.js route-level error.tsx — use the right tool per failure type.

Quick index

#Section
1Definition & purpose
2Default React behavior
3Implementation requirements
4Application strategies
5Retry logic pattern
6Debugging & logging
7Limits & best practices

1. Definition & purpose

An Error Boundary is a React component that wraps a subtree and catches errors thrown during render, in lifecycle methods, or in constructors of children below it.

PurposeMeaning
Catches rendering errorsIntercepts throws in child render / lifecycle
Replaces crashed subtreeOnly the wrapped region fails — not the whole app
Fallback UI providerShows recovery UI instead of a white screen
Prevents blank screensUsers stay in a working shell with a clear message
plaintext
App shell (always visible)
├── Sidebar ✓
├── ErrorBoundary
│   └── RevenueChart ✗ throws → fallback "Chart unavailable"
└── ActivityFeed ✓

Tip

Think of boundaries as circuit breakers for UI — isolate failure to the smallest region that still makes sense for the user (one widget, one panel, one route section).

Back to index


2. Default React behavior

Without boundaries, React treats the UI as one nested tree — an uncaught error in a leaf can tear down everything above it.

BehaviorConsequence
UI as component treeErrors originate in one node, affect ancestors
Child crash affects subtreeFailed child unmounts its branch
Errors bubble upPropagate until caught or reach root
Unmanaged errors crash appRoot unmount → blank screen ("white screen of death")

Without a boundary

tsx
function BrokenChart() {
  const data = null as { values: number[] } | null
  // Throws during render — no boundary → entire Dashboard unmounts
  return <svg>{data!.values.map((v) => v)}</svg>
}
 
function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <BrokenChart />
      <ActivityFeed /> {/* never renders if chart throws */}
    </div>
  )
}

With a boundary — sibling survives

tsx
function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <ErrorBoundary fallback={<p>Chart failed to load.</p>}>
        <BrokenChart />
      </ErrorBoundary>
      <ActivityFeed /> {/* still renders */}
    </div>
  )
}

Warning

A single boundary at the app root only catches everything but replaces the entire page on any error — prefer granular boundaries around risky widgets so navigation and unrelated content survive.

Back to index


3. Implementation requirements

Error Boundaries must be class components — there is no useErrorBoundary hook in core React (libraries like react-error-boundary wrap this for ergonomics).

RequirementDetail
Class componentFunctional components cannot be boundaries (yet)
getDerivedStateFromErrorMap error → state for fallback render
componentDidCatchSide effects — log, report to Sentry
StatehasError, optional error, optional retryKey
Propschildren, fallback or render prop

Full implementation

tsx
'use client'
 
import { Component, type ErrorInfo, type ReactNode } from 'react'
 
type ErrorBoundaryProps = {
  children: ReactNode
  fallback?: ReactNode | ((error: Error, retry: () => void) => ReactNode)
  onError?: (error: Error, info: ErrorInfo) => void
}
 
type ErrorBoundaryState = {
  hasError: boolean
  error: Error | null
  retryKey: number
}
 
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  state: ErrorBoundaryState = {
    hasError: false,
    error: null,
    retryKey: 0,
  }
 
  static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
    return { hasError: true, error }
  }
 
  componentDidCatch(error: Error, info: ErrorInfo) {
    console.error('[ErrorBoundary]', error.message, info.componentStack)
    this.props.onError?.(error, info)
  }
 
  handleRetry = () => {
    this.setState((prev) => ({
      hasError: false,
      error: null,
      retryKey: prev.retryKey + 1,
    }))
  }
 
  render() {
    const { hasError, error, retryKey } = this.state
    const { children, fallback } = this.props
 
    if (hasError && error) {
      if (typeof fallback === 'function') {
        return fallback(error, this.handleRetry)
      }
      return fallback ?? <p role="alert">Something went wrong.</p>
    }
 
    // retryKey forces child remount on retry — see section 5
    return <div key={retryKey}>{children}</div>
  }
}

Note

getDerivedStateFromError must be pure — no side effects. Logging belongs in componentDidCatch only.

Warning

Error Boundaries must be class components in React today. Do not attempt to replicate boundary semantics with try/catch around {children} in a function component — render errors do not propagate to parent try/catch blocks.

react-error-boundary alternative

tsx
'use client'
 
import { ErrorBoundary } from 'react-error-boundary'
 
function ChartFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) {
  return (
    <div role="alert">
      <p>Chart error: {error.message}</p>
      <button type="button" onClick={resetErrorBoundary}>
        Try again
      </button>
    </div>
  )
}
 
export function RevenueSection() {
  return (
    <ErrorBoundary FallbackComponent={ChartFallback} onError={logToSentry}>
      <RevenueChart />
    </ErrorBoundary>
  )
}

Back to index


4. Application strategies

Multiple boundaries throughout the app beat one root wrapper.

StrategyGuidance
Avoid root-onlyRoot boundary = whole page fallback on any bug
Multiple boundaries OKWrap each risky island independently
Granular placementCharts, third-party widgets, experiment flags
Sibling component ruleIndependent siblings → separate boundaries

Sibling component rule

RelationshipBoundary strategy
IndependentSeparate boundaries — one's crash does not hide the other
InterdependentShared boundary — if A needs B, wrap both together
tsx
function AnalyticsDashboard() {
  return (
    <div className="grid grid-cols-2 gap-4">
      {/* Independent widgets — separate boundaries */}
      <ErrorBoundary fallback={<WidgetError label="Revenue" />}>
        <RevenueChart />
      </ErrorBoundary>
 
      <ErrorBoundary fallback={<WidgetError label="Users" />}>
        <UserGrowthChart />
      </ErrorBoundary>
 
      {/* Interdependent pair — shared boundary */}
      <ErrorBoundary fallback={<WidgetError label="Funnel" />}>
        <FunnelFilters />
        <FunnelChart />
      </ErrorBoundary>
    </div>
  )
}
 
function WidgetError({ label }: { label: string }) {
  return (
    <div className="rounded border border-dashed p-6 text-center text-sm text-neutral-500">{label} unavailable</div>
  )
}

Layered boundaries — page + widget

tsx
function ReportsPage() {
  return (
    <ErrorBoundary fallback={<PageError />}>
      <PageHeader />
      <div className="grid gap-6">
        <ErrorBoundary fallback={<p>Export panel failed.</p>}>
          <ExportPanel />
        </ErrorBoundary>
        <ErrorBoundary fallback={<p>Analytics failed.</p>}>
          <AnalyticsGrid />
        </ErrorBoundary>
      </div>
    </ErrorBoundary>
  )
}

Tip

Wrap third-party chart libraries, markdown renderers, and A/B experiment branches in boundaries first — they are the highest-risk render crash sources in production dashboards.

Back to index


5. Retry logic pattern

Let users recover without a full page refresh by resetting boundary state and remounting children with a new key.

StepAction
handleRetry methodClear hasError, increment retryKey
Increment retryKeyState change triggers fresh child mount
Force re-render via keyReact discards failed subtree state
Avoid full refreshSmoother UX than window.location.reload()
tsx
'use client'
 
function ChartFallback({ error, onRetry }: { error: Error; onRetry: () => void }) {
  return (
    <div className="rounded-lg border border-red-200 bg-red-50 p-4" role="alert">
      <p className="font-semibold text-red-800">Could not load chart</p>
      <p className="mt-1 text-sm text-red-600">{error.message}</p>
      <button type="button" className="mt-3 rounded bg-red-800 px-3 py-1 text-white" onClick={onRetry}>
        Try again
      </button>
    </div>
  )
}
 
function RevenueWidget() {
  return (
    <ErrorBoundary
      fallback={(error, retry) => <ChartFallback error={error} onRetry={retry} />}
      onError={(error, info) => reportError({ error, componentStack: info.componentStack })}>
      <RevenueChart />
    </ErrorBoundary>
  )
}

Why retryKey matters

tsx
// Inside ErrorBoundary render — child remounts when retryKey changes
return <div key={retryKey}>{children}</div>

Without a key change, a child with corrupted internal state might throw again immediately on retry. Incrementing key forces React to destroy and recreate the subtree — equivalent to a local soft refresh.

Note

Retry resets client render state only. If the failure was a bad API response that will fail again, pair retry with refetch logic inside the child or pass a key derived from query dataUpdatedAt.

Back to index


6. Debugging & logging

componentDidCatch receives everything needed for dev consoles and production monitoring.

DataSourceUse
Error messageerror.messageUser-facing copy, alert titles
Component stackinfo.componentStackPinpoint which component failed
Console loggingcomponentDidCatchLocal dev debugging
Monitoring SDKSentry, Datadog, etc.Production error tracking
tsx
componentDidCatch(error: Error, info: ErrorInfo) {
  if (process.env.NODE_ENV === 'development') {
    console.group('[ErrorBoundary] Render crash')
    console.error('Message:', error.message)
    console.error('Stack:', error.stack)
    console.error('Component stack:', info.componentStack)
    console.groupEnd()
  }
 
  reportToSentry({
    level: 'error',
    message: error.message,
    extra: {
      componentStack: info.componentStack,
      digest: error.digest, // Next.js may attach digest on server errors
    },
  })
}

Structured error report helper

tsx
type ErrorReport = {
  error: Error
  componentStack: string
  boundary: string
  route?: string
}
 
export function reportError({ error, componentStack, boundary, route }: ErrorReport) {
  // Replace with your monitoring provider
  console.error(JSON.stringify({ boundary, route, message: error.message, componentStack }))
 
  if (typeof window !== 'undefined' && 'Sentry' in window) {
    // Sentry.captureException(error, { contexts: { react: { componentStack } } })
  }
}

Performance

Log once per boundary catch in componentDidCatch — not on every render of the fallback UI. Avoid heavy serialization of large props in error reports.

Tip

Tag reports with a boundary name prop — boundary="RevenueChart" — so dashboards group failures by widget, not just by error message string.

Back to index


7. Limits & best practices

What Error Boundaries do NOT catch

Failure typeCorrect handling
Event handler errorstry/catch inside handler
Async code (setTimeout, fetch).catch() or try/catch in async fn
Server Components errorsNext.js error.tsx at route segment
Errors in the boundary itselfParent boundary or route fallback
SSR during initial renderFramework error pages / error.tsx
tsx
'use client'
 
function SaveButton() {
  async function handleClick() {
    try {
      await saveProfile()
    } catch (error) {
      toast.error('Save failed') // boundary will NOT catch this
    }
  }
 
  return (
    <button type="button" onClick={handleClick}>
      Save
    </button>
  )
}

Next.js App Router — route-level recovery

tsx
// app/dashboard/error.tsx
'use client'
 
export default function DashboardError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
  return (
    <div role="alert">
      <h2>Dashboard failed to load</h2>
      <p>{error.message}</p>
      <button type="button" onClick={reset}>
        Try again
      </button>
    </div>
  )
}

Warning

Do not rely on Error Boundaries for data-fetch errors returned as state (if (error) return <p>…</p>) — those are not thrown. Boundaries catch throws during render, not error flags from TanStack Query.

Interview Answer

"Error Boundary vs Suspense fallback?"

Suspense handles expected loading — lazy components and async data that suspend intentionally. Error Boundaries handle unexpected throws — bugs, null access, third-party render failures. Use both: Suspense for loading skeletons, Error Boundary around the same region for crash recovery.

Tip

Production checklist

  • Class component or react-error-boundary
  • Pure getDerivedStateFromError; log in componentDidCatch
  • Granular boundaries around risky widgets
  • Independent siblings → separate boundaries
  • Retry via retryKey + child key remount
  • Report with component stack to monitoring
  • try/catch for handlers and async — not boundaries
  • Next.js error.tsx for route segments
  • Meaningful fallback copy + retry action

Back to index


Summary

PieceRole
Error BoundaryCatches render errors in child subtree
Fallback UIReplaces crashed region — app shell survives
componentDidCatchLog and report with component stack
Retry + keyRemount subtree without page reload
Granular placementIsolate widgets — not root-only

Error Boundaries turn catastrophic white screens into localized, recoverable failures. Implement them as class components (or via react-error-boundary), place them around risky islands, and pair with handler try/catch and Next.js error.tsx for complete coverage.

Next reads: Facade Pattern for isolating risky data orchestration, Performance Guide (Part 1) for render optimization, and React Design Patterns for Suspense fallback boundaries.

Share this article

XLinkedInFacebook
Kazi Rahamatullah

Written by

Kazi Rahamatullah

FullStack Developer

X / TwitterGitHubLinkedIn

Subscribe to my newsletter

Stay up to date and get notified when I share new contents.

No spam ever, unsubscribe anytime