React Interview Prep
Patterns and Best Practices

Error Boundaries

Catch Crashes Before They Break Your Entire App

LinkedIn Hook

One unhandled error in a single component can white-screen your entire React application. The user sees nothing. No navigation, no error message, no way to recover. Just a blank page and a cryptic console error that nobody outside your dev team will ever read.

Error boundaries exist to prevent exactly this. They are React's built-in mechanism for catching JavaScript errors during rendering, in lifecycle methods, and in constructors of the component tree below them. When a child component throws, the error boundary catches it and renders a fallback UI instead of unmounting the entire tree.

But here is the part that trips up almost every candidate in interviews: error boundaries can only be class components. There is no hook equivalent. You cannot build one with useEffect or try-catch inside a function component. React deliberately chose not to add a useErrorBoundary hook because of how the rendering model works.

Interviewers will ask: "Your production app crashes when the API returns unexpected data. How do you prevent the entire page from going blank?" They want to hear you explain getDerivedStateFromError for updating state, componentDidCatch for logging, fallback UI design, and where to place boundaries for the right granularity. They also want to know you understand the react-error-boundary library and when to use it.

In this lesson, I cover everything: what error boundaries are, why they must be class components, how getDerivedStateFromError and componentDidCatch work together, fallback UI patterns, granularity strategies for placing boundaries, the react-error-boundary library that simplifies everything, and why React has no hook equivalent.

If your React app has zero error boundaries — every crash is a full white-screen. This lesson fixes that.

Read the full lesson -> [link]

#React #JavaScript #InterviewPrep #Frontend #ErrorBoundaries #ErrorHandling #WebDevelopment #CodingInterview #100DaysOfCode


Error Boundaries thumbnail


What You'll Learn

  • What error boundaries are and why they exist in React
  • Why error boundaries must be class components and cannot be hooks
  • How getDerivedStateFromError and componentDidCatch work together
  • How to design meaningful fallback UI for crashed components
  • How to decide where to place error boundaries for the right granularity
  • How the react-error-boundary library simplifies error boundary usage in modern React

The Concept — Contain the Blast Radius

Analogy: Fire Doors in a Building

Imagine a 20-story office building with no fire doors. Every floor is connected by open hallways, open stairwells, open elevator shafts. If a fire starts in the kitchen on the 5th floor, it spreads everywhere. Every floor fills with smoke. Everyone evacuates the entire building. The whole operation shuts down because of one kitchen fire.

Now imagine the same building with fire doors. The 5th floor kitchen catches fire, but the fire doors slam shut. The fire is contained to that one section. The rest of the 5th floor keeps working. Floors 1-4 and 6-20 never even notice. The fire department deals with one contained area while the building keeps functioning.

That is what error boundaries do in React. Without them, one component throwing an error during rendering brings down the entire component tree. The user sees a white screen. With error boundaries, the crash is contained. The broken component shows a fallback message like "Something went wrong" while the rest of the application — navigation, sidebar, other features — keeps working.

getDerivedStateFromError is the fire door slamming shut — it immediately updates state to switch to the fallback UI. componentDidCatch is the fire alarm calling the fire department — it logs the error to your monitoring service so you know what happened. The fallback UI is the sign on the fire door that says "This section is temporarily closed" — it tells the user something went wrong without destroying their entire experience.

The key decision is where to install fire doors. Put them on every single room and you waste money and slow down movement. Put only one at the building entrance and a small fire still takes down everything inside. The right answer is strategic placement — around major sections, around areas known to be risky, and around boundaries between independent features.


Error Boundaries — Class Components Only

An error boundary is any class component that defines either static getDerivedStateFromError() or componentDidCatch() (or both). When a descendant component throws during rendering, React walks up the tree looking for the nearest error boundary. If it finds one, it calls these methods instead of unmounting the entire app.

Code Example 1: Building a Basic Error Boundary

import React from "react";

// Error boundaries MUST be class components
// There is no function component or hook equivalent
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    // This state controls whether we show the fallback or the children
    this.state = { hasError: false, error: null };
  }

  // PHASE 1: Called during rendering — update state to trigger fallback UI
  // This is a static method — it has no access to 'this'
  // It receives the error and returns new state
  static getDerivedStateFromError(error) {
    // Returning state here causes React to re-render with the fallback
    return { hasError: true, error };
  }

  // PHASE 2: Called after rendering — used for side effects like logging
  // This receives the error AND an info object with the component stack trace
  componentDidCatch(error, errorInfo) {
    // Log to an external error monitoring service (Sentry, DataDog, etc.)
    console.error("Error caught by boundary:", error);
    console.error("Component stack:", errorInfo.componentStack);

    // In production, you would send this to your monitoring service:
    // logErrorToService(error, errorInfo);
  }

  render() {
    // If an error was caught, render the fallback UI
    if (this.state.hasError) {
      return (
        <div style={{ padding: "20px", background: "#1a1a2e", color: "#ff6b6b" }}>
          <h2>Something went wrong</h2>
          <p>This section encountered an error and could not render.</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>
            Try Again
          </button>
        </div>
      );
    }

    // If no error, render children normally
    return this.props.children;
  }
}

// A component that will crash when it receives bad data
function UserProfile({ user }) {
  // If user is null, accessing user.name throws a TypeError during rendering
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// Using the error boundary to protect the app
function App() {
  return (
    <div>
      <h1>My Application</h1>
      <ErrorBoundary>
        {/* If UserProfile crashes, only this section shows the fallback */}
        <UserProfile user={null} />
      </ErrorBoundary>
      <footer>This footer always renders, even if UserProfile crashes</footer>
    </div>
  );
}

export default App;

// Output (what the user sees):
// My Application
// ┌─────────────────────────────────────┐
// │ Something went wrong                │
// │ This section encountered an error   │
// │ and could not render.               │
// │ [Try Again]                         │
// └─────────────────────────────────────┘
// This footer always renders, even if UserProfile crashes

// Console output:
// Error caught by boundary: TypeError: Cannot read properties of null (reading 'name')
// Component stack:
//   at UserProfile
//   at ErrorBoundary
//   at div
//   at App

Key point: getDerivedStateFromError runs during the render phase (synchronous, no side effects). componentDidCatch runs during the commit phase (safe for side effects like logging). Use both together: one for the fallback, one for reporting.


Granularity — Where to Place Boundaries

Placement of error boundaries determines how much of your app breaks when something goes wrong. Too few boundaries and a small error takes down everything. Too many boundaries and your UI becomes fragmented with error messages.

Code Example 2: Strategic Error Boundary Placement

import React from "react";

// Reusable error boundary with customizable fallback
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error(`[${this.props.name}] Error:`, error.message);
  }

  render() {
    if (this.state.hasError) {
      // Use custom fallback if provided, otherwise show default
      return this.props.fallback || <p>Something went wrong in this section.</p>;
    }
    return this.props.children;
  }
}

// Simulating components that might crash
function Header() {
  return <nav>Navigation Bar</nav>;
}

function Sidebar() {
  return <aside>Sidebar Links</aside>;
}

function Feed({ posts }) {
  // This crashes if posts is undefined
  return posts.map((post) => <div key={post.id}>{post.title}</div>);
}

function Chat() {
  // This might crash due to WebSocket issues
  return <div>Live Chat Widget</div>;
}

function App() {
  return (
    <div>
      {/* LEVEL 1: Top-level boundary — catches anything that escapes */}
      {/* This is your last line of defense before the white screen */}
      <ErrorBoundary
        name="App"
        fallback={
          <div style={{ padding: "40px", textAlign: "center" }}>
            <h1>Application Error</h1>
            <p>Please refresh the page.</p>
          </div>
        }
      >
        {/* Header is critical — if it crashes, we want to know immediately */}
        <Header />

        <div style={{ display: "flex" }}>
          <Sidebar />

          {/* LEVEL 2: Feature-level boundary around the main content */}
          {/* If the feed crashes, sidebar and header stay intact */}
          <ErrorBoundary
            name="Feed"
            fallback={<p>Could not load your feed. Please try again later.</p>}
          >
            <Feed posts={undefined} />
          </ErrorBoundary>

          {/* LEVEL 3: Widget-level boundary for independent features */}
          {/* Chat is non-critical — if it crashes, the app is still usable */}
          <ErrorBoundary
            name="Chat"
            fallback={<p>Chat is temporarily unavailable.</p>}
          >
            <Chat />
          </ErrorBoundary>
        </div>
      </ErrorBoundary>
    </div>
  );
}

export default App;

// Output (what the user sees):
// Navigation Bar
// ┌──────────────────┬──────────────────────────────────────┬──────────────────┐
// │ Sidebar Links    │ Could not load your feed.            │ Live Chat Widget │
// │                  │ Please try again later.              │                  │
// └──────────────────┴──────────────────────────────────────┴──────────────────┘

// The Feed crashed, but:
// - Header still works
// - Sidebar still works
// - Chat still works
// - Only the Feed section shows the fallback

// Console:
// [Feed] Error: Cannot read properties of undefined (reading 'map')

Key point: Use a layered strategy. A top-level boundary as the last line of defense. Feature-level boundaries around major sections. Widget-level boundaries around independent, non-critical features that can fail gracefully.


The react-error-boundary Library

Writing class components just for error boundaries feels out of place in a codebase full of function components and hooks. The react-error-boundary library wraps this pattern in a clean API with reset capabilities, retry logic, and callback props.

Code Example 3: Using react-error-boundary

import { ErrorBoundary } from "react-error-boundary";
import { useState } from "react";

// A fallback component that receives error details and a reset function
// This is a regular function component — no class needed
function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div style={{ padding: "20px", background: "#2d1b1b", borderRadius: "8px" }}>
      <h3>Something went wrong</h3>
      <p style={{ color: "#ff6b6b" }}>{error.message}</p>
      {/* Clicking retry resets the boundary and re-renders the children */}
      <button onClick={resetErrorBoundary}>Retry</button>
    </div>
  );
}

// A component that crashes when count reaches 3
function BuggyCounter() {
  const [count, setCount] = useState(0);

  if (count === 3) {
    throw new Error("Counter crashed at 3!");
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>My App</h1>

      <ErrorBoundary
        FallbackComponent={ErrorFallback}
        onError={(error, errorInfo) => {
          // Log to your error monitoring service
          console.error("Caught by react-error-boundary:", error);
        }}
        onReset={() => {
          // Called when resetErrorBoundary is triggered
          // Use this to reset any state that caused the error
          console.log("Boundary reset — retrying render");
        }}
        resetKeys={["someKey"]}
        // resetKeys: if any value in this array changes, the boundary auto-resets
        // Useful for resetting when the user navigates or data changes
      >
        <BuggyCounter />
      </ErrorBoundary>
    </div>
  );
}

export default App;

// User clicks Increment three times:
// Count: 0 -> Count: 1 -> Count: 2 -> CRASH
//
// Output after crash:
// My App
// ┌──────────────────────────────────────┐
// │ Something went wrong                 │
// │ Counter crashed at 3!                │
// │ [Retry]                              │
// └──────────────────────────────────────┘
//
// Console: Caught by react-error-boundary: Error: Counter crashed at 3!
//
// User clicks Retry:
// Console: Boundary reset — retrying render
// Counter re-renders fresh at Count: 0

Key point: react-error-boundary gives you FallbackComponent (a function component for the fallback), onError (for logging), onReset (for cleanup), and resetKeys (for automatic reset). It eliminates the need to write class components while providing more features than a manual error boundary.


Why There Is No Hook Equivalent

This is one of the most asked interview questions about error boundaries. React hooks cannot catch errors during rendering because of how the rendering model works.

Code Example 4: Why try-catch Does Not Work in Function Components

import { useState, useEffect } from "react";

// DOES NOT WORK: try-catch cannot catch errors during rendering of children
function BrokenErrorBoundary({ children }) {
  const [hasError, setHasError] = useState(false);

  // PROBLEM 1: try-catch in the function body only catches errors
  // in THIS component's rendering logic, not in children
  try {
    if (hasError) {
      return <p>Something went wrong</p>;
    }
    return children;
    // If a CHILD component throws during rendering, this try-catch
    // does NOT catch it. The error happens when React renders the children,
    // which is AFTER this function returns.
  } catch (error) {
    // This never runs for child component errors
    // It only catches errors in the lines above within THIS function
    setHasError(true);
  }
}

// PROBLEM 2: useEffect cannot catch render errors either
function AlsoBrokenBoundary({ children }) {
  useEffect(() => {
    // useEffect runs AFTER rendering is complete
    // If rendering threw an error, useEffect never runs at all
    // There is nothing to catch here
  });

  return children;
}

// WHY class components can do it:
// React's reconciler has special handling for class components.
// When rendering a class component's subtree throws, React catches it
// internally and calls getDerivedStateFromError / componentDidCatch
// on the nearest class-based error boundary.
//
// This is implemented in React's fiber reconciler — not in JavaScript
// try-catch. Function components return JSX, and React renders their
// children later. By the time a child throws, the parent function
// has already returned.
//
// The React team has discussed adding a useErrorBoundary hook but
// has not shipped one because the semantics are complex:
// - Should it catch errors in the same component or only children?
// - How does it interact with concurrent rendering?
// - How does reset work with closures and stale state?
//
// Until React adds native support, use:
// 1. A class component error boundary (manual)
// 2. The react-error-boundary library (recommended)

Key point: The rendering model is the reason. When a function component returns JSX containing children, React renders those children in a separate phase. A try-catch in the parent function body cannot catch errors that happen in a later rendering phase. Class components have special integration with React's fiber reconciler that function components do not.


Error Boundaries visual 1


Error Boundaries visual 2


Common Mistakes

Mistake 1: Expecting error boundaries to catch event handler errors

class ErrorBoundary extends React.Component {
  // ... standard getDerivedStateFromError and componentDidCatch

  render() {
    if (this.state.hasError) return <p>Caught an error!</p>;
    return this.props.children;
  }
}

function ButtonComponent() {
  const handleClick = () => {
    // This error is thrown INSIDE an event handler
    // Error boundaries do NOT catch this — it is not a rendering error
    throw new Error("Button click failed!");
  };

  return <button onClick={handleClick}>Click me</button>;
}

// BAD: Assuming the ErrorBoundary catches the click error
function App() {
  return (
    <ErrorBoundary>
      <ButtonComponent />
      {/* The error from handleClick is NOT caught by the boundary */}
      {/* It becomes an unhandled error — use try-catch in the handler */}
    </ErrorBoundary>
  );
}

// GOOD: Use regular try-catch for event handlers
function FixedButtonComponent() {
  const handleClick = () => {
    try {
      throw new Error("Button click failed!");
    } catch (error) {
      // Handle it manually — show a toast, update state, log it
      console.error("Handled event error:", error.message);
    }
  };

  return <button onClick={handleClick}>Click me</button>;
}

// Error boundaries catch: rendering errors, lifecycle method errors,
//   constructor errors in the tree below them
// Error boundaries do NOT catch: event handlers, async code (setTimeout,
//   fetch), server-side rendering, errors in the boundary itself

Mistake 2: Placing a single error boundary only at the root level

// BAD: One boundary at the root means ANY error replaces the ENTIRE app
function App() {
  return (
    <ErrorBoundary fallback={<p>The entire app crashed. Refresh the page.</p>}>
      <Header />
      <Sidebar />
      <MainContent />
      <ChatWidget />
      <Footer />
    </ErrorBoundary>
  );
  // If ChatWidget crashes, the user loses Header, Sidebar, MainContent,
  // and Footer — all replaced by a single error message
}

// GOOD: Wrap independent sections in their own boundaries
function App() {
  return (
    <ErrorBoundary fallback={<p>Critical app error. Refresh the page.</p>}>
      <Header />
      <Sidebar />
      <ErrorBoundary fallback={<p>Content failed to load.</p>}>
        <MainContent />
      </ErrorBoundary>
      <ErrorBoundary fallback={<p>Chat unavailable.</p>}>
        <ChatWidget />
      </ErrorBoundary>
      <Footer />
    </ErrorBoundary>
  );
  // ChatWidget crashes? Only chat shows the fallback. Everything else works.
}

Mistake 3: Forgetting that error boundaries cannot catch their own errors

// BAD: Error boundary tries to do complex rendering in its fallback
// If the fallback itself throws, there is nothing to catch it
class RiskyBoundary extends React.Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  render() {
    if (this.state.hasError) {
      // If THIS code throws, the boundary cannot catch its own error
      // It bubbles up to the next boundary or causes a white screen
      return <ComplexErrorDashboard error={this.state.error} />;
    }
    return this.props.children;
  }
}

// GOOD: Keep fallback UI simple and unlikely to throw
class SafeBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      // Simple, static fallback — almost impossible to crash
      return (
        <div style={{ padding: "20px" }}>
          <p>Something went wrong.</p>
          <button onClick={() => this.setState({ hasError: false })}>
            Try Again
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

Interview Questions

Q: What is an error boundary in React and why is it needed?

An error boundary is a React class component that catches JavaScript errors in its child component tree during rendering, lifecycle methods, and constructors. Without error boundaries, a single rendering error in any component unmounts the entire React tree, leaving the user with a blank white screen. Error boundaries catch these errors and render a fallback UI instead, keeping the rest of the application functional. They act as a containment layer — similar to try-catch, but specifically for React's declarative rendering model.

Q: What is the difference between getDerivedStateFromError and componentDidCatch?

getDerivedStateFromError is a static method called during the render phase. It receives the error and returns an object to update state — typically setting a flag to switch to the fallback UI. It must be pure with no side effects. componentDidCatch is an instance method called during the commit phase (after the DOM has updated). It receives both the error and an errorInfo object containing the component stack trace. It is used for side effects like logging errors to a monitoring service. In practice, you use getDerivedStateFromError to display the fallback and componentDidCatch to report the error.

Q: Why can't error boundaries be function components? Why is there no useErrorBoundary hook?

Error boundaries require special integration with React's fiber reconciler. When a child component throws during rendering, React internally catches the error and looks up the tree for the nearest class component with getDerivedStateFromError or componentDidCatch. Function components return JSX and React renders their children in a later phase — a try-catch in the parent function body cannot catch errors that happen during child rendering. The React team has discussed a useErrorBoundary hook but has not shipped one because of complex semantics around concurrent rendering, whether it should catch errors in the same component or only children, and how reset behavior interacts with closures. Until React adds this, the react-error-boundary library provides a clean API that wraps a class component internally.

Q: What types of errors do error boundaries NOT catch?

Error boundaries do not catch errors in event handlers (use regular try-catch), asynchronous code (setTimeout, requestAnimationFrame, fetch promises), server-side rendering, or errors thrown in the error boundary component itself. They only catch errors that occur during React's rendering phase — in render methods, lifecycle methods, and constructors of descendant components. For event handler errors, you must use standard JavaScript try-catch. For async errors, you handle them in .catch() blocks or try-catch inside async functions.

Q: How would you decide where to place error boundaries in a production application?

Use a layered strategy with three levels. First, a top-level boundary around the entire app as the last line of defense — its fallback is a "please refresh" message. Second, feature-level boundaries around major independent sections like the main content area, sidebar, and navigation. Third, widget-level boundaries around non-critical features that can fail gracefully — chat widgets, recommendation panels, analytics dashboards. The goal is to minimize blast radius: a crash in the chat widget should not take down the navigation. Place boundaries around components that consume external data (API responses), use third-party libraries, or are known to be error-prone. Avoid wrapping every single component — that creates excessive nesting and fragmented error states.


Quick Reference — Cheat Sheet

ERROR BOUNDARIES
================

What They Are:
  Class components that catch rendering errors in their child tree
  Prevent one broken component from white-screening the entire app
  Show fallback UI instead of unmounting everything

Two Key Methods:
  getDerivedStateFromError(error)     → render phase, returns new state
  componentDidCatch(error, info)      → commit phase, for logging/side effects

Basic Structure:
  class ErrorBoundary extends React.Component {
    state = { hasError: false };
    static getDerivedStateFromError(error) { return { hasError: true }; }
    componentDidCatch(error, info) { logToService(error, info); }
    render() {
      if (this.state.hasError) return <Fallback />;
      return this.props.children;
    }
  }

What They Catch:
  + Rendering errors in child components
  + Lifecycle method errors in child components
  + Constructor errors in child components

What They Do NOT Catch:
  - Event handlers (use try-catch)
  - Async code (setTimeout, fetch, promises)
  - Server-side rendering errors
  - Errors in the boundary itself

Granularity Strategy:
  +----------------------------+------------------------------------------+
  | Level                      | Purpose                                  |
  +----------------------------+------------------------------------------+
  | App-level (root)           | Last defense before white screen         |
  | Feature-level (sections)   | Isolate major independent areas          |
  | Widget-level (components)  | Non-critical features that can fail      |
  +----------------------------+------------------------------------------+

react-error-boundary Library:
  import { ErrorBoundary } from "react-error-boundary";
  <ErrorBoundary
    FallbackComponent={MyFallback}      // Function component for fallback
    onError={(error, info) => log()}    // Error logging callback
    onReset={() => cleanup()}           // Called on reset/retry
    resetKeys={[key]}                   // Auto-reset when keys change
  >
    <ComponentThatMightCrash />
  </ErrorBoundary>

Why No Hook Equivalent:
  - Function components return JSX; children render in a later phase
  - try-catch in parent cannot catch child rendering errors
  - React's reconciler has special class component handling
  - Discussed by React team but not shipped due to complexity

Reset Patterns:
  Manual:    <button onClick={() => setState({ hasError: false })}>Retry</button>
  Library:   resetErrorBoundary() passed to FallbackComponent
  Auto:      resetKeys={[userId]} — resets when userId changes

Previous: Lesson 9.3 — Compound Components Pattern -> Next: Lesson 10.1 — Testing React Components ->


This is Lesson 9.4 of the React Interview Prep Course — 10 chapters, 42 lessons.

On this page