Error Boundaries

Reviewed & published by Brayan K

An error boundary is a class component that catches render-time errors in its children and shows a fallback UI instead of crashing the whole app to a blank screen. It's React's try/catch for the component tree. By the end you'll build one, know exactly what it can and can't catch, and place boundaries to keep your app resilient.

Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

1️⃣ Why Boundaries Exist

If any component throws during render and nothing catches it, React unmounts the entire tree — the user sees a blank white page. An error boundary catches that error, shows a friendly fallback, and keeps the rest of the app running. It's defined as a class with two special methods.

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  // 1) Flip state to render the fallback on the next render.
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  // 2) Side effects: log the error to a monitoring service.
  componentDidCatch(error, info) {
    logToService(error, info);
  }

  render() {
    if (this.state.hasError) return <p>Something went wrong.</p>;
    return this.props.children;   // normal path: render the children
  }
}

The demo models that try/catch behavior — a healthy child renders, a throwing child falls back:

// An error boundary CATCHES a render error and shows a fallback instead of
// crashing. We model its state machine: render child, catch, show fallback.

function renderWithBoundary(renderChild, fallback) {
  try {
    return renderChild();              // try to render the child
  } catch (err) {
    // getDerivedStateFromError would set hasError = true; we return fallback.
    return fallback + " (" + err.message + ")";
  }
}

// A child that renders fine:
console.log(renderWithBoundary(() => "✅ Profile loaded", "⚠️ Something broke"));
// ✅ Profile loaded

// A child that throws during render:
console.log(renderWithBoundary(() => { throw new Error("bad data"); }, "⚠️ Something broke"));
// ⚠️ Something broke (bad data)
// The app stays alive — only the broken subtree is replaced.

Your turn — complete the catch so a crashing child returns the fallback:

// 🎯 YOUR TURN — fill in each ___ then run it.
// Finish the boundary: when the child throws, return the fallback.

function boundary(renderChild, fallback) {
  try {
    return renderChild();
  } catch (err) {
    // 1) The child crashed — show the fallback instead of letting it bubble up.
    return ___;        // 👉 fallback
  }
}

console.log(boundary(() => "Widget OK", "🛟 Fallback shown"));            // works
console.log(boundary(() => { throw new Error("boom"); }, "🛟 Fallback shown")); // crashes

// ✅ Expected output:
//    Widget OK
//    🛟 Fallback shown

2️⃣ Using a Boundary

Wrap any part of your tree that might fail. Everything inside is protected; if it throws during render, the boundary's fallback appears in its place while the rest of the page stays interactive.

function App() {
  return (
    <div>
      <Header />                               
      <ErrorBoundary>
        <RiskyDashboard />                     
      </ErrorBoundary>
      <Footer />                               
    </div>
  );
}

Crucially, boundaries only catch render-phase errors in children. They do not catch errors in event handlers, in setTimeout/promises, or thrown by the boundary itself — those still need a regular try/catch.

3️⃣ Granular Boundaries Isolate Failures

A single top-level boundary is a good safety net, but a crash there still replaces the whole screen. Place smaller boundaries around independent widgets so one failure only blanks that widget — the rest of the dashboard keeps working.

function Dashboard() {
  return (
    <div className="grid">
      <ErrorBoundary><Weather /></ErrorBoundary>
      <ErrorBoundary><Stocks /></ErrorBoundary>    
      <ErrorBoundary><News /></ErrorBoundary>       
    </div>
  );
}

The demo models that isolation — one broken widget falls back while its neighbors render fine:

// GRANULAR boundaries isolate failures: wrap each widget so one crash
// doesn't blank the whole page. We render a list, each item in its own try.

function renderWidget(widget) {
  try {
    if (widget.broken) throw new Error("render failed");
    return "✅ " + widget.name;
  } catch {
    return "⚠️ " + widget.name + " unavailable";   // local fallback
  }
}

const widgets = [
  { name: "Weather", broken: false },
  { name: "Stocks",  broken: true  },   // this one crashes
  { name: "News",    broken: false },
];

widgets.map(renderWidget).forEach(line => console.log(line));
// ✅ Weather
// ⚠️ Stocks unavailable     <- only this widget fell back
// ✅ News
// The other widgets keep working — that's the value of granular boundaries.

Reorder Challenge

These lines define an error boundary class. Put them in a correct order:

A.   static getDerivedStateFromError() { return { hasError: true }; }
B.   class ErrorBoundary extends React.Component {
C.   render() { return this.state.hasError ? <Fallback /> : this.props.children; }
D.   state = { hasError: false };
E.   }

Quick Recall

1) A button's onClick handler throws. Does the error boundary catch it?

No. Boundaries catch only render-phase errors. Event handler errors need a regular try/catch.

2) Which method flips state to show the fallback, and which is for side effects like logging?

static getDerivedStateFromError updates state; componentDidCatch handles side effects (logging).

3) Why use several small boundaries instead of one at the root?

To isolate failures — a crash in one widget falls back locally while the rest of the UI stays alive.

Common Errors (and the fix)

📋 Quick Reference

PieceRole
getDerivedStateFromErrorreturn { hasError: true } // show fallback
componentDidCatchlog(error, info) // side effects
Use it<ErrorBoundary><Risky /></ErrorBoundary>
Not caughtevents, async, the boundary itself

Mini-Challenge: Keep the Page Alive

Model a boundary around three widgets where the middle one throws. Render each through the boundary and log the lines. The healthy widgets should still appear — predict the output first.

function renderWidget(w) {
  try {
    if (w.broken) throw new Error("fail");
    return "✅ " + w.name;
  } catch {
    return "⚠️ " + w.name + " unavailable";
  }
}

const widgets = [
  { name: "Map", broken: false },
  { name: "Chat", broken: true },
  { name: "Tasks", broken: false },
];
// your code here: map widgets through renderWidget and log each line

🎉 Lesson Complete

Practice quiz

What is an error boundary in React?

  • A hook that catches errors
  • A try/catch around fetch calls
  • A class component that catches render-time errors in its children and shows a fallback UI
  • A Suspense boundary

Answer: A class component that catches render-time errors in its children and shows a fallback UI. An error boundary is a class component that catches errors thrown by children during rendering and shows a fallback.

Why must an error boundary be a class component?

  • The catching lifecycle methods exist only on class components
  • Classes render faster
  • Function components can't have state
  • It is just a convention

Answer: The catching lifecycle methods exist only on class components. getDerivedStateFromError and componentDidCatch are class-only lifecycle methods; there's no hook equivalent yet.

Which method updates state to show the fallback on the next render?

  • componentDidCatch
  • componentDidMount
  • render
  • static getDerivedStateFromError

Answer: static getDerivedStateFromError. static getDerivedStateFromError(error) returns new state (e.g. { hasError: true }) to render the fallback.

Which method is intended for side effects like logging the error?

  • getDerivedStateFromError
  • componentDidCatch
  • render
  • constructor

Answer: componentDidCatch. componentDidCatch(error, info) handles side effects such as logging to a monitoring service.

Does an error boundary catch errors thrown inside an event handler?

  • No — it catches only render-phase errors in children
  • Yes, always
  • Only in production
  • Only for onClick

Answer: No — it catches only render-phase errors in children. Boundaries catch render-phase errors. Event handler errors need ordinary try/catch.

Which of these does an error boundary NOT catch?

  • A render error in a child component
  • An error during a child's render method
  • An error in async code like setTimeout or a promise
  • A throw in a child's JSX expression

Answer: An error in async code like setTimeout or a promise. Boundaries don't catch errors in event handlers, async code, SSR, or the boundary itself.

What happens by default if a component throws during render and nothing catches it?

  • React shows a red box and continues
  • React unmounts the entire tree — the user sees a blank screen
  • Only that component is hidden
  • Nothing visible changes

Answer: React unmounts the entire tree — the user sees a blank screen. An uncaught render error unmounts the whole tree, leaving a blank page — which boundaries prevent.

Why place several granular boundaries instead of one at the root?

  • It is required by React
  • To make the bundle smaller
  • To avoid using classes
  • To isolate failures so one widget's crash doesn't blank the whole app

Answer: To isolate failures so one widget's crash doesn't blank the whole app. Granular boundaries fall back locally, keeping the rest of the UI alive when one section crashes.

What should getDerivedStateFromError return?

  • The error object
  • New state, e.g. { hasError: true }
  • A fallback element
  • undefined

Answer: New state, e.g. { hasError: true }. It returns a state update (like { hasError: true }) so render can show the fallback.

An error boundary's hasError stays true and never recovers. What's a fix?

  • Nothing can reset it
  • Delete componentDidCatch
  • Provide a retry that resets state or remount via a key
  • Make it a function component

Answer: Provide a retry that resets state or remount via a key. Offer a retry that resets hasError, or remount the subtree via a changing key, to recover.

Continue this course

Related lessons