Handling errors gracefully in react with error boundaries

Prerequisites to implement Error boundaries in React

  • Understanding of error types such as run-time, and compile-time errors.
  • Knowledge of Class based components.
  • An sentry account for logging errors

Topics covered in this blogpost

  1. Architectural design pattern for implementing error boundaries in react.
  2. Implementation of error boundary from scratch.
  3. Types of errors catched by error boundaries.
  4. Common problems faced during the usage of react error boundary.
  5. react-error-boundary to the rescue.
  6. Implementation of third-party error logging tools such as sentry.

Architecture of our error boundary:

  • In react, all the error boundaries are made up of class based components.
  • Error boundaries are some of the graceful ways using which you can catch errors in a more efficient way.
  • You can consider it as a try and catch blocks of JSX ecosystem.
  • Below is a simple example of error boundaries in react
const App = () => {

  return (
    <div>
    <h1>Counter Example</h1>
      <ErrorBoundary fallBackUIComponent={<FallBackUI />}>
        <BuggyComponent />
      </ErrorBoundary>
    </div>
  );
}
  • As you can see ErrorBoundary component is placed as a parent to a component which we suspect might cause an error.
  • Whenever a run-time error occurs in the BuggyComponent the nearest error boundary which is ErrorBoundary component catches it and displays a fallback UI. Below Gif will explain this scenario.
  • Since the error boundary is a class based component therefore it has certain methods which it uses to catch errors. Below is the architectural diagram of the ErrorBoundary: Alt Text

Implementation of Error Boundary:

  • Before Implementing the error boundary we should keep in mind the following things:

    • Error boundary is always a class based component.
    • It uses following two methods to catch the errors:
      • static getDerivedStateFromError(): A static method which is executed before the DOM is ready(during the rendering phase of the component). This will get invoked whenever descendant component throws an error.
      • componentDidCatch(): This will get invoked whenever a descendant component throws an error. This component is called during commit phase i.e. When the DOM is ready. It can be used to perform side-effects in the component. It receives two parameters:
        • error - error that is being thrown.
        • info - An object with componentStack which tells us which component threw an error.
  • Now we can move towards the implementation of the error boundary. Below code will demonstrate a class based react error boundary:

class ErrorBoundary extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      hasError: false
    };
  }
   static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }
  componentDidCatch(error, errorInfo) {
    //Can be used to log to any logging service like sentry
     console.log("Catched error", errorInfo);
  }

  render(){
    if(this.state.hasError){
      return(
        // <h3>Something went wrong!</h3>
        //Can be a static or a fall-back component passed as a prop.
        this.props.fallBackUIComponent
      );
    }

    return this.props.children;
  }
}

Few things to note in the above implementation:

  1. getDerivedStateFromError returns a value to update the state of the component in this case hasError is set to true.
  2. componentDidCatch will also catch the error along with the stack trace of the error. This will occur on the commit phase of the component.

Lastly in the render function if the hasError state is true then this will print our fallback component which we passed it as a prop. Else it will return the children.

Usage of this component is fairly simple. Just wrap the compontent in the question with the ErrorBoundary Component so that it catches the error thrown by it's descendant. Below example will give you a clear idea of it's usage:

//Component for fallback UI:
const FallBackUI = () => {
  return (
    <>
    <h3>Something went wrong</h3>
      </>
  );
}

const BuggyComponent = () => {
  const [count, setCount] = React.useState(0);

  const increaseCounter = () => {
    setCount(preVal => preVal + 1);
  }
    if(count === 5) {
      throw new Error("Crashing the app!!");
    }

  return (
    <>
      <div className="counter--block">
        <span>Counter</span>
        <span>{count}</span>
      </div>
      <button onClick={increaseCounter}>Increase count</button>
      </>
  );
}

const App = () => {

  return (
    <div>
    <h1>Counter Example</h1>
      <ErrorBoundary fallBackUIComponent={<FallBackUI />}>
        <BuggyComponent />
      </ErrorBoundary>
    </div>
  );
}

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

Types of error catched by error boundaries

  1. React's Error Boundary documentation clearly states that it catches only the errors which occur during the life-cycle of a component i.e. It will catch only run-time errors.
  2. Below mentioned errors are not being catched by react's error boundaries:

    Event handlers (learn more)

    Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)

    Server side rendering

    Errors thrown in the error boundary itself (rather than its children)

Common problems faced during the usage of react error boundary:

There might be couple of reasons for error boundary not to work.

Some of them are mentioned below:

Placement of ErrorBoundary component.

There are some cases where we forget that the component needs to be always wrapped with the ErrorBoundary component so that it catches error. Below example will provide clear understanding:

Consider a component which will throw an error when the counter value reaches 5:

const BuggyComponent = () => {
  const [count, setCount] = React.useState(0);

  const increaseCounter = () => {
    setCount(preVal => preVal + 1);
  }
    if(count === 5) {
      throw new Error("Crashing the app!!");
    }

  return (
    <>
      <div className="counter--block">
        <span>Counter</span>
        <span>{count}</span>
      </div>
      <button onClick={increaseCounter}>Increase count</button>
      </>
  );
}

Placing the error boundary like below will never allow the ErrorBoundary Component to catch error, since the BuggyComponent is not being wrapped with ErrorBoundary but rather the content of this component is wrapped with ErrorBoundary.

return (
    <ErrorBoundary>
      <div className="counter--block">
        <span>Counter</span>
        <span>{count}</span>
      </div>
      <button onClick={increaseCounter}>Increase count</button>
      </ErrorBoundary>
  );

And also neither any of this will capture the error throw by BuggyComponent. To make this work we can do something like this:

const App = () => {

  return (
    <div>
    <h1>Counter Example</h1>
      <ErrorBoundary>
        <BuggyComponent />
      </ErrorBoundary>
    </div>
  );
}

Now the ErrorBoundary will catch the error thrown by the BuggyComponent since it is being wrapped by the error boundary.

Trying to throw a new error from event handler:

In the above usecase as you have seen whenever the count value reaches 5 it will throw a new error.

Note: The if block for this is placed in the rendering phase of the component because of which it creates a valid case for ErrorBoundary to catch the error.

const BuggyComponent = () => {
  const [count, setCount] = React.useState(0);

  const increaseCounter = () => {
    setCount(preVal => preVal + 1);
  }
    if(count === 5) {
      throw new Error("Crashing the app!!");
    }

  return (
    <>
      <div className="counter--block">
        <span>Counter</span>
        <span>{count}</span>
      </div>
      <button onClick={increaseCounter}>Increase count</button>
      </>
  );
}

const App = () => {

  return (
    <div>
    <h1>Counter Example</h1>
      <ErrorBoundary>
        <BuggyComponent />
      </ErrorBoundary>
    </div>
  );
}

But the same won't work if you place the if block inside the increaseCounter function. The above example is altered to showcase this scenario:

const BuggyComponent = () => {
  const [count, setCount] = React.useState(0);

  const increaseCounter = () => {
    setCount(preVal => preVal + 1);
    if(count === 5) {
      throw new Error("Crashing the app!!");
    }
  }

  return (
    <>
      <div className="counter--block">
        <span>Counter</span>
        <span>{count}</span>
      </div>
      <button onClick={increaseCounter}>Increase count</button>
      </>
  );
}

const App = () => {

  return (
    <div>
    <h1>Counter Example</h1>
      <ErrorBoundary>
        <BuggyComponent />
      </ErrorBoundary>
    </div>
  );
}

Alternative packages: react-error-boundary

react-error-boundary is a pretty impressive package. It solves most of the challenges faced by react's error boundary where it won't be able to catch errors such as errors thrown from event handlers, asynchornous code etc.
You can refer to the package's github readme for more information.

Below is the implmentation of the above example but using react-error-boundary:

import {ErrorBoundary} from 'react-error-boundary';

function ErrorFallback({error}) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre style={{color: 'red'}}>{error.message}</pre>
    </div>
  )
}

const BuggyCounter = () => {
  const [count, setCount] = React.useState(0);

  const handleIncrement = () => {
    setCount(preVal => preVal + 1);
  }

  if(count === 5){
      throw new Error("New Crashing Seq. Initiated");
  }

  return(
    <div className="counter--block">
      <span>Count</span>
      <span>{count}</span>
      <button onClick={handleIncrement}>Increment count</button>
      </div>
  );
}
const App = () => {
  return(
    <>
    <h1>Counter Example</h1>
    <ErrorBoundary FallbackComponent={ErrorFallback}>

        <BuggyCounter />
    </ErrorBoundary>
      </>
  )
}

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

Implmentation of third-party error logging tools

Error logging is a crucial part of any application development process. It helps us to analyze and organize errors which are not catched during the testing process of the application. These error logging tools can generally be used to moniter the errors which are thrown on the client's machine/browser.

When it comes to error logging I find sentry.io to be a bliss. It has pretty impressive documentation and has wide range of support on different tech stacks such as Java, JS, React, React-Native etc.

Below is the modified example of the above example.

import React from "react";
import ReactDOM from "react-dom";
import * as Sentry from "@sentry/react";
import App from "./App";

Sentry.init({ dsn: "https://[email protected]/0" });

const BuggyCounter = () => {
    const [counter, setCounter] = useState(0);

    return (
        <>
        <div className="counter--value">
            {counter}
        </div>
        <div>
            <button
              className="counter--button"
              onClick={() => { throw new Error("New Test Error")}}>
                increment count
             </button>
        </div>
        </>
    )
}

const App = () => {
  return (
    <Sentry.ErrorBoundary fallback={"An error has occurred"}>
      <BuggyCounter />
    </Sentry.ErrorBoundary>
  );
}

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

// Can also use with React Concurrent Mode
// ReactDOM.createRoot(document.getElementById('root')).render(<App />);

In this example you need to first initialize the Sentry's instance with init function:

Sentry.init({ dsn: "https://[email protected]/0" });

NOTE: dsn is data source name which tells the SDK where to send the events.

Sentry also provides it's own error boundary component.

import * as Sentry from "@sentry/react";

const App = () => {
  return (
    <Sentry.ErrorBoundary fallback={"An error has occurred"}>
      <BuggyCounter />
    </Sentry.ErrorBoundary>
  );
}

You can find the code used in this blogpost below:

  1. Implementation of react error boundary from scratch:
    https://codepen.io/keyurparalkar/pen/LYWJKvm?editors=0010

  2. Implementation of react error boundary using react-error-boundary package:
    https://codepen.io/keyurparalkar/pen/bGqQNJe

Feel free to reach out to me @

21