21/25 lessons84%

Building a Custom Error Boundary Component

If you're like most modern React devs, you've probably written 98% of your code using function components and hooks. And that’s great. But the moment you try to build a custom error boundary, React slams on the brakes and says, “Sorry pal, hooks don’t work here.” You’re forced to dust off those old class skills and jump back into lifecycle methods like it’s 2018. Annoying? A bit. Necessary? Absolutely. This lesson is about building your own battle-tested, customizable error boundary. One that doesn’t just show a sad emoji but helps you log, recover, and restore your app like a pro.

Class Components Still Matter (Here’s Why)

Look, I’m team hooks all day. useState, useEffect, useReducer—love 'em.

But there are some places where class components still rule, and error boundaries are the biggest one. React's error-handling lifecycle only works in class components. You can’t throw useEffect(() => { try {} catch {} }) and expect it to catch render-time errors. Doesn’t work. Never has.

That’s because error boundaries rely on two lifecycle methods that don’t exist in function components:

`getDerivedStateFromError` componentDidCatch

And there’s no hook-based equivalent—yet. There have been proposals (and third-party workarounds), but nothing solid in core React.

So for now? You build error boundaries like your granddad built UIs—with class and state.

Defining `getDerivedStateFromError` and `componentDidCatch` 🔍

These are the two keys to the kingdom.

`getDerivedStateFromError`

This static method is how you react (pun intended) to a thrown error and update your UI.

javascript
1
2
3
          static getDerivedStateFromError(error) {
  return { hasError: true };
}
        

It runs right before render when an error is thrown. You use it to flip a switch and show a fallback UI instead of letting React try to render the broken stuff again.

`componentDidCatch`

This one is all about side effects. Logging, metrics, sending angry Slack messages.

javascript
1
2
3
          componentDidCatch(error, errorInfo) {
  console.error("Error caught by boundary:", error, errorInfo);
}
        

You get the error itself and a whole treasure chest of info: stack trace, component stack, etc.

Together, they make a clean, bulletproof error boundary:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
          class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

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

  componentDidCatch(error, errorInfo) {
    // log somewhere fancy
  }

  render() {
    if (this.state.hasError) {
      return <h2>Oops! Something broke.</h2>;
    }

    return this.props.children;
  }
}
        

Yes, it’s boilerplate. But it works, and it works well.

Logging Errors to External Tools (Your Future Self Will Thank You)

Here’s a hard truth: if your error boundary isn’t logging the errors somewhere, then it’s just hiding bugs. That “friendly” fallback UI might save the user, but now you’re blind to what failed.

In my last SaaS gig, we had a boundary that looked nice but did zero logging. Guess what? A third of users couldn’t load their dashboards for three days. We only found out from a tweet.

That’s why you need something like Sentry, LogRocket, or even just a REST endpoint that stores stack traces in your DB. Hook it up in componentDidCatch:

javascript
1
2
3
4
5
6
          componentDidCatch(error, info) {
  logErrorToService({
    error: error.toString(),
    stack: info.componentStack,
  });
}
        

Now your users get a fallback, and you get a breadcrumb trail.

Showing Fallback UIs (Without Freaking Out Users)

The fallback UI matters. A lot. And not just visually.

You want something that:

Explains something went wrong. Doesn’t feel like a total crash. * Gives the user a way to recover or report the issue.

Bad:

javascript
1
          return <h1>Something went wrong</h1>;
        

Better:

javascript
1
2
3
4
5
6
          return (
  <div className="error-boundary">
    <h2>Whoa, something blew up.</h2>
    <p>We're working on it. Try refreshing the page.</p>
  </div>
);
        

Add your brand’s tone. Make it match the app. Don’t use scary red screens unless it’s really that bad.

And if you have the resources, integrate a “Send Report” button that pings your backend with the error info. Users love that.

Enhancing Boundaries with Reset Buttons and Navigation

The best error boundaries don’t just catch and whimper. They recover.

Sometimes an error is a fluke—bad data, temporary auth issue, etc. You can offer a “Try Again” button that resets the error state:

javascript
1
          this.setState({ hasError: false });
        

But here’s a twist: you probably want to use navigation to refresh the component tree instead. Especially in apps using React Router.

javascript
1
          <button onClick={() => navigate("/")}>Back to Home</button>
        

Even better? Combine both:

javascript
1
2
3
4
5
6
          <button onClick={() => {
  this.setState({ hasError: false });
  navigate("/dashboard");
}}>
  Retry Dashboard
</button>
        

I once had a client-side dashboard where a missing API token would throw on mount. With a reset button and a forced re-fetch, we let users recover without reloading the page—and support tickets dropped overnight.

Now that your error boundaries are smart, logged, styled, and interactive, it’s time to figure out why your app is slow and which components are dragging things down. Head over to Meet React DevTools Profiler and let’s start profiling like a detective.