Handling Errors with Error Boundary

Avatar of Kingsley Silas
Kingsley Silas on

📣 Freelancers, Developers, and Part-Time Agency Owners: Kickstart Your Own Digital Agency with UACADEMY Launch by UGURUS 📣

Thinking and building in React involves approaching application design in chunks, or components. Each part of your application that performs an action can and should be treated as a component. In fact, React is component-based and, as Tomas Eglinkas recently wrote, we should leverage that concept and err on the side of splitting any large chunking into smaller components.

Splitting inevitably introduces component hierarchies, which are good because they bloated components and architecture. However, things can begin to get complicated when an error occurs in a child component. What happens when the whole application crashes?! Seriously, React, why do the parent and sibling components have to pay for the sins of another component? Why?

Error Boundaries

React 16 came with a lot of goodies, one of which is error boundaries. Let’s consult the documentation and break down what it says about this gem because we can use it to spot errors where they occur and resolve them faster and with less headache!

Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed. Error boundaries catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them.

That’s a lot of jargon but, like components, we can break it down into less complex chunks.

Error boundaries are React Components

This makes a lot of sense and useful because it’s a concept we have using all along. The difference is that juice was sprayed on it to make it different from a normal component. Still, don’t forget the basic idea that error boundaries are themselves React Components!

Error boundaries catch JavaScript errors anywhere in their child component tree

In case you have forgotten how children component tree work, here is an example:

<ParentComponent>
  <FirstChild>
    <FirstChildDaughter>
    </FirstChildDaughter>
  </FirstChild>
  <SecondChild>
  </SecondChild>
</ParentComponent>

We have two parent and three child components. According to what we have learned so far about error boundaries, we can replicate the above tree to:

<ErrorBoundaryComponent>
  <ParentComponent>
    <FirstChild>
      <FirstChildDaughter>
      </FirstChildDaughter>
    </FirstChild>
    <SecondChild>
    </SecondChild>
  </ParentComponent>
</ErrorBoundaryComponent>

By wrapping the whole tree up in an ErrorBoundaryComponent, we can catch any JavaScript errors that occur in its child components. Cool, right?

Error boundaries log those errors

When errors are caught, we want boundaries errors to do something with them, preferably something to tell us about the. Developers often make use of error logging platforms to monitor errors that occur on their software. With error boundaries, we can do the same.

Error boundaries display a fallback UI

Instead of displaying the whole annoying combo of reds in different shades, you can choose a customized user interface to display when an error occurs. That can come in super handy because it allows you to tailor errors in a style that makes it easier for you to read and scan. Super cool, right?

Like me, you’ll think that this means error boundaries will catch all JavaScript errors. Sadly, that’s not true. Here are errors that they will gracefully ignore:

  • Event handlers
  • Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
  • Server-side rendering
  • Errors thrown in the error boundary itself (rather than its children)

componentDidCatch()

The extra juice that makes a component an error boundary is componentDidCatch() — this is a lifecycle method that works like the JavaScript catch{} block, but for components. When an error is found in a child component, the error is handled by the closest error boundary. Only class components can be error boundaries.

componentDidCatch() accepts two parameters:

  • error: This is the error that was thrown
  • info: An object which contains a trace of where the error occurred

Error Boundary In Action

Say we are working on a feature that lists locations where conferences can be held. Something like this:

See the Pen error boundary 0 by Kingsley Silas Chijioke (@kinsomicrote) on CodePen.

The application lists locations from the Location component and the individual locations are output as Location Cards. We take a little extra care to ensure the name of each location is rendered in uppercase for consistency. For this tutorial purpose, we will add an empty object to the list of locations.

class Location extends React.Component {
  state = {
    locations: [
      {
        "name": "Ojo",
        "zone": "Lagos State",
        "region": "South West"
      },
      {
        "name": "Ahiazu Mbaise",
        "zone": "Imo State",
        "region": "South East"
      },
      {
        "name": "Akoko-Edo",
        "zone": "Edo State",
        "region": "South South"
      },
      {
        "name": "Anka",
        "zone": "Zamfara State",
        "region": "North West"
      },
      {
        "name": "Akwanga",
        "zone": "Nasarawa State",
        "region": "North Central"
      },
      {
        
      }
    ]
  }
  render() {
    return (
      <div>
        <div>
          <div>
            <h2>Locations</h2>
          </div>
        </div>
        <div>
          {this.state.locations
            .map(location => 
              <LocationCard key={location.id} {...location} />
          )}
        </div>
      </div>
    )
  }
}

const LocationCard = (props) => {
  return (
    <div>
      <hr />
      <p><b>Name:</b> {props.name.toUpperCase()}</p>
      <p><b>Zone:</b> {props.zone}</p>
      <p><b>Region:</b> {props.region}</p>
      <hr />
    </div>
  )
}

const App = () => (
  <div>
     <Location />
  </div>
)

ReactDOM.render(<App />, document.getElementById("root"));

If you run this in the browser, you will see an error similar to this screenshot:

A screenshot of the Type Error providing the error message Cannot read property toUpperCase of undefined. Background color is tan and there is a block of code with a light red background indicating where the error is in the code base.

That’s not totally helpful, so let’s apply an error boundary to handle help us out. First, we’ll create an ErrorBoundary component:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
      info: null
    };
  }
  componentDidCatch(error, info) {
    this.setState({
      hasError: true,
      error: error,
      info: info
    });
  }
  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h1>Oops, something went wrong :(</h1>
          <p>The error: {this.state.error.toString()}</p>
          <p>Where it occured: {this.state.info.componentStack}</p>
        </div>
      );
    }
    return this.props.children;
  }
}

An initial state for hasError, error, and info is created. Then, the componentDidCatch() lifecycle method is added. If an error occurs in the constructor, render or lifecycle method of any of its children components, the hasError state will be changed to true. When this happens, the ErrorBoundary component renders and displays the error. But if there are no errors, the children of the ErrorBoundary component are rendered instead as we’d expect.

Next, we need to add both the ErrorBoundary and Location components to our main App component:

const App = () => (
  <div>
    <ErrorBoundary>
      <Location />
    </ErrorBoundary>
  </div>
)

See the Pen error boundary 2 by Kingsley Silas Chijioke (@kinsomicrote) on CodePen.

We don’t see that annoying TypeError UI anymore! Things are working!

There’s one little thing we can do to improve the app. If you check the code in the demo, you’ll see an empty object we added at the end. Is it possible to have the other credible locations render? Most definitely! Inside the Location component, we can wrap the LocationCard component with the ErrorBoundary component to scope error logging directly to the cards:

class Location extends React.Component {
  state = {
    locations: [
      {
        "name": "Ojo",
        "zone": "Lagos State",
        "region": "South West"
      },
      {
        "name": "Ahiazu Mbaise",
        "zone": "Imo State",
        "region": "South East"
      },
      {
        "name": "Akoko-Edo",
        "zone": "Edo State",
        "region": "South South"
      },
      {
        "name": "Anka",
        "zone": "Zamfara State",
        "region": "North West"
      },
      {
        "name": "Akwanga",
        "zone": "Nasarawa State",
        "region": "North Central"
      },
      {
        // Empty!
      }
    ]
  }
  render() {
    return (
      <div>
        <div>
          <div>
            <h2>Locations</h2>
          </div>
        </div>
        <div>
          {this.state.locations
            .map(location => 
            <ErrorBoundary>
              // Should render all locations, but the empty instance
              <LocationCard key={location.id} {...location} />
            </ErrorBoundary>
          )}
        </div>
      </div>
    )
  }
}

This time, the credible locations show, except the one that is empty. You can choose to wrap the whole component tree with an error boundary component once, or you can wrap different components at strategic places. The decision is up to you.

Wrapping Up

I encourage you to start making use of error boundaries in your applications. Similarly, it’s worth digging in a little deeper. For that, here are some issues in the React repo on Error Boundaries and event handles, go through them so you can see the current state of where things are at: