Skip to main content

Data and Errors

Error Boundaries

Written by Published

An uncaught error during render does not break one component - React unmounts the entire tree, and the user gets a blank page. An error boundary is the only thing that stops that.

Why a blank page

React cannot know which parts of a half-rendered tree are still trustworthy, so it takes the safe option and removes everything. That is deliberate, and it means one careless user.name on a null user can take down the whole app.

A boundary catches it

An error boundary is a component that catches errors thrown while rendering anything below it and shows a fallback instead. It is the one thing still written as a class - there is no hook equivalent, because the lifecycle it needs has none.

A boundary and a crash

jsx

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props)
    this.state = { error: null }
  }

  // Called when a child throws during render.
  static getDerivedStateFromError(error) {
    return { error }
  }

  render() {
    if (this.state.error) {
      return (
        <div style={{ border: "1px solid #fecaca", background: "#fef2f2", padding: 10, borderRadius: 8 }}>
          <strong>Something went wrong.</strong>
          <p style={{ margin: "6px 0 0", color: "#b91c1c" }}>{this.state.error.message}</p>
          <button onClick={() => this.setState({ error: null })}>Try again</button>
        </div>
      )
    }
    return this.props.children
  }
}

function Risky({ crash }) {
  if (crash) throw new Error("A component threw while rendering")
  return <p style={{ color: "#15803d" }}>Rendering normally.</p>
}

function Demo() {
  const [crash, setCrash] = React.useState(false)

  return (
    <div>
      <button onClick={() => setCrash(true)}>Make it crash</button>
      <p>The rest of the page keeps working.</p>

      <ErrorBoundary>
        <Risky crash={crash} />
      </ErrorBoundary>
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Demo />)

Press the button: the risky component is replaced by the fallback, and everything outside the boundary carries on. Without it, the whole preview would go blank.

What it does not catch

  • Event handlers - use try/catch; nothing is rendering when a click fires.
  • Async code - a rejected promise or a setTimeout callback is outside the render pass.
  • Errors in the boundary itself - it cannot catch its own.
  • Server rendering - a different mechanism applies there.

So a boundary is for rendering failures. A failed request is not one - that is the error state from the previous lesson, handled by ordinary code.

Where to put them

One at the root stops the blank page. Additional ones around independent regions - a sidebar, a widget, a route - keep a failure local, so a broken chart does not take the navigation with it.

In real projects most people use react-error-boundary rather than writing the class, since it adds retry and reset handling. Knowing what the class does first makes that library obvious rather than magic.