Understanding React's Error Boundaries

In the past, Javascript errors inside components used to corrupt React’s internal state and produce a corrupted UI in place. React did not provide any way to handle these errors gracefully, and could not recover from them.

An important change was introduced in React version 16. Now any uncaught Javascript error will result in unmounting the whole React component tree. This leaves no room for corrupted UIs.

Why was this change introduced? Well, the React team believes that it is a bad practice to leave a corrupted UI in place, because it may have dangerous outcomes. For example, in a bank app, it is worse to display a wrong account balance than to render nothing.

That sounds logical, but still a Javascript error in some part of the UI shouldn’t break the whole app. To solve this problem, the React team introduced a new concept called error boundary.

What is an error boundary?

Error boundaries are React components. Their role is to catch Javascript errors anywhere in their child component tree, log them and display an alternative UI instead of the component tree that crashed.

Error boundaries catch errors during:

  • Rendering
  • Lifecycle methods
  • Constructors

But it’s important to know that error boundaries do not catch errors for:

  • Event handlers
  • Async code
  • Server side rendering (SSR)
  • Errors thrown in the error boundary itself

Error boundaries work like a Javascript catch {} block, but for components.

Before we create an error boundary component…

We need some app to test it. So let’s create a very simple UI where we have two sections: News and Chat.

Both sections have a button that will simulate a Javascript error when clicked.

This is our News component:

import React from "react";

const styles = {
  newsBox: {
    border: "1px solid #333",
    margin: "0.5rem 0",
    height: "50px",
    width: "300px",
  },
};

const News = () => {
  const [error, setError] = React.useState(false);

  const handleClick = () => {
    setError(true);
  };

  if (error) throw new Error("News error!");

  return (
    <>
      <h2>News</h2>
      <div style={styles.newsBox} />
      <div style={styles.newsBox} />
      <button onClick={handleClick}>Throw Error</button>
    </>
  );
};

export default News;

The Chat component, very similar to the previous one:

import React from "react";

const Chat = () => {
  const styles = {
    chatBox: {
      border: "1px solid #333",
      margin: "0.5rem 0",
      height: "150px",
      width: "300px",
    },
  };

  const [error, setError] = React.useState(false);

  const handleClick = () => {
    setError(true);
  };

  if (error) throw new Error("News error!");

  return (
    <>
      <h2>Chat</h2>
      <div style={styles.chatBox} />
      <button onClick={handleClick}>Throw Error</button>
    </>
  );
};

export default Chat;

And our App component:

import React from "react";
import News from "./News";
import Chat from "./Chat";

export default function App() {
  return (
    <div style={{ padding: "0.5rem 1.5rem" }}>
      <h1>Welcome!</h1>
      <hr />
      <News />
      <hr />
      <Chat />
    </div>
  );
}

Our app looks like this:

0
Our simple application

Now let’s see what happens when a Javascript error is thrown.

Without error boundaries

If we click one of the Throw Error buttons, we would expect the whole app to break. As we previously discussed, React 16 has this behavior for any uncaught Javascript error.

And effectively, it does break:

Our whole app breaks

Now let’s see how to catch these errors with an error boundary component.

How to create an error boundary component

Creating an error boundary component is very easy. The first thing you should know is that error boundaries have to be class components. Right now there is no way to create an error boundary using a functional component.

import React from 'react';

export default class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return...
  }
}

The next thing you should do is adding the componentDidCatch method. This method receives two parameters: the error and the errorInfo.

As we want to display a fallback UI in case of error, we need to have some state that indicates that. So let’s add it, and update the state when an error is caught:

import React from 'react';

export default class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null, errorInfo: null };
  }

  componentDidCatch(error, errorInfo) {
    this.setState({
      error: error,
      errorInfo: errorInfo,
    });

    // Log error info somewhere
  }

  render() {
    return ...
  }
}

You could also log the errorInfo somewhere.

Great! The last step is to complete the render function. We want to render a fallback UI if there is an error. Otherwise, we just need to render the children.

import React from "react";

export default class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null, errorInfo: null };
  }

  componentDidCatch(error, errorInfo) {
    this.setState({
      error: error,
      errorInfo: errorInfo,
    });

    // Log error info somewhere
  }

  render() {
    if (this.state.errorInfo) {
      return <h2>Something went wrong!</h2>;
    }
    return this.props.children;
  }
}

Perfect! We have our error boundary component, now let’s see how we can use it.

Catching errors with an error boundary component

Using our new error boundary component is very easy. We just need to include it in our component tree as a wrapper:

export default function App() {
  return (
    <ErrorBoundary>
      <div style={{ padding: "0.5rem 1.5rem" }}>
        <h1>Welcome!</h1>
        <hr />
        <News />
        <hr />
        <Chat />
      </div>
    </ErrorBoundary>
  );
}

This way, when a Javascript error is thrown, it will be caught by our error boundary and the fallback UI will be displayed instead:

Our error boundary component catches the error

Using multiple error boundaries

The previous example worked fine, we were able to catch our error and display a fallback UI.

However, our entire application was replaced by this error message, even though the error was thrown by only one of the components.

The good news is that you can have multiple error boundaries for different sections of your application.

For example, Facebook Messenger wraps content of the sidebar, the info panel, the conversation log, and the message input into separate error boundaries. If some component in one of these UI areas crashes, the rest of them remain interactive.

In our case, we could use an error boundary for our News component, and another one for our Chat component:

export default function App() {
  return (
    <div style={{ padding: "0.5rem 1.5rem" }}>
      <h1>Welcome!</h1>
      <hr />
      <ErrorBoundary>
        <News />
      </ErrorBoundary>
      <hr />
      <ErrorBoundary>
        <Chat />
      </ErrorBoundary>
    </div>
  );
}

Now, if our News section throws a Javascript error, our Chat section won’t be affected:

Multiple error boundary components catch different errors

And this is the real power of error boundaries. If you use multiple of them, you can isolate errors in different, independent sections, without affecting the whole application.

That’s it!

I hope this post was helpful and now you understand better what error boundaries are and how to use them. Thanks for reading! ❤️