Aman Scripts

Aman Scripts

Learn React: How to use Error Boundaries

Learn React: How to use Error Boundaries

Subscribe to my newsletter and never miss my upcoming articles

Prefer a Video Instead?

What is an Error Boundary?

Error boundaries are exactly what they sound like: they catch errors from falling through your front-end application. Introduced in later versions of React JS, they are almost necessary for any errors thrown in your front-end components.

Error boundaries also help you in many ways; they can almost always reduce the need for duplicate code. Errors are shown everywhere in your application, and you must have a state variable for "error" in almost all your components.

Well, this is no longer necessary if you wrap your component in an Error Boundary. Error boundaries look like this, when abstracted out. Any error thrown by ArticleSearch or by its children, will be caught.

<ErrorBoundary>
    <ArticleSearch />
</ErrorBoundary>

No Error, vs Error: ErrorBoundary Image

How to make an Error Boundary?

The concept is simple: create a class component, with the following methods:

  • static getDerivedStateFromError(error)
    • This method should return your new state for the ErrorBoundary Component.
  • componentDidCatch(error, errorInfo)
    • This method is for any logic that you need to perform after the error has been caught. For example, you can log to your server so that the error does not stop at the client side.
  • render()
    • You will actually display your error in the render method to the user, if you need to.

So far, we have to have some logic in all of these methods, let's see what the logic is in each one. For getDerivedStateFromError, it has one job: to return the next state of your component when the error is caught. For this, we actually need to have some state. Let's create some interfaces for our class ErrorBoundary class component.

interface ErrorBoundaryProps {
  onError?: (error: Error) => void;
  onRetry?: () => void;
}

interface ErrorBoundaryState {
  message: string;
}

export class ErrorBoundary 
  extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
  ...
}

So then, our render method can look for the message variable in our state, since that is the error message. There are two conditions we have: there is an error, or there is not an error. So, we can solve this by one if/else statement.

  • If there is an error, we display the error content
  • Else, we can display the children passed into the Error Boundary.

    render() {
      if (this.state.message) {
        return (
          <div className="error-message">
            <div>{this.state.message}</div>
            <button onClick={this.handleRetry}>Try Again</button>
          </div>
        );
      }
    
      return this.props.children;
    }
    

Since we need to actually set the state somewhere, we will use the static method mentioned above. We will just return the "message" property of our error, so that can be set to the state.

  static getDerivedStateFromError(error) {
    return { message: error.message };
  }

In our componentDidCatch method, we can call the props onError callback so that the parent can know when there is an error. Remember, this is optional, so we need to check if it exists or not.

  componentDidCatch(error, errorInfo) {
    if (this.props.onError) {
      this.props.onError(error);
    }
  }

Our render method also demands a retry, so we need to call the onRetry callback from the parent. But make sure you also clear the error so that if there is a new error, it can be shown!

  handleRetry = () => {
    this.setState({ message: "" });
    this.props.onRetry();
  };

To make our errors pretty, we can also add a .css file as below:

import './ErrorBoundary.css';
.error-message {
  padding: 20px;
  background-color: #ff9c9c;
  color: black;
  border-radius: 20px;
}

Updating ArticleSearch component

If you have read my blog post about useData Hook in React, you know that it does not throw any errors. This is a problem that we need to deal with, because it will never get to the ErrorBoundary!

But it is pretty simple, the useData Hook will set the error in the error variable in the constructed array:

  const [data, query, setQuery, loading, error] = useData(
    "https://hn.algolia.com/api/v1/search"
  );

And, in our component, what we can do is watch the error variable. If it exists, we can throw a new error, so that the ErrorBoundary can catch it.

  useEffect(() => {
    if (error) {
      throw new Error(error);
    }
  }, [error]);

There are also some other exceptions for ErrorBoundaries that you have to handle on your own:

  • Errors in the Error Boundary class it self
  • Asynchronous code (For example, API Calls)
  • Event handlers, such as "onClick"
  • Server Side Rendered is not supported

That's It!

You can use this error boundary as a parent wrapper component in any of your components, and your errors will start showing up on the UI, in a similar fashion. This greatly reduces your application state variables, as well as duplicated code.

Full Code:

import React from "react";
import "./ErrorBoundary.css";

interface ErrorBoundaryProps {
  onError?: (error: Error) => void;
  onRetry?: () => void;
}

interface ErrorBoundaryState {
  message: string;
}

export class ErrorBoundary extends React.Component<
  ErrorBoundaryProps,
  ErrorBoundaryState
> {
  constructor(props) {
    super(props);
    this.state = {
      message: ""
    };
  }

  static getDerivedStateFromError(error) {
    return { message: error.message };
  }

  componentDidCatch(error, errorInfo) {
    if (this.props.onError) {
      // log to server
      this.props.onError(error);
    }
  }

  handleRetry = () => {
    this.setState({ message: "" });
    this.props.onRetry();
  };

  render() {
    if (this.state.message) {
      return (
        <div className="error-message">
          <div>{this.state.message}</div>
          <button onClick={this.handleRetry}>Try Again</button>
        </div>
      );
    }

    return this.props.children;
  }
}

CSS:

.error-message {
  padding: 20px;
  background-color: #ff9c9c;
  color: black;
  border-radius: 20px;
}
 
Share this