React Error Handling Made Easy with Component-Based Strategies
Tags: javascript, react, tutorial, typescript
Published: 2022-12-25 16:16:47

## Introduction

In JavaScript we can throw different things, from errors, to booleans, even objects. React components are no exception and similar to JavaScript errors, they end up bubble up and if they are not handled properly, they can cause inconvenience to the user, such as breaking a page.

In the past I wrote how to protect our routes from errors using Error Boundaries at the route level, in today's article I'm going to share a more granular solution that can be used at different levels of the component tree.

With a more granular approach, we can encapsulate some components in order to prevent an error from bubbling up.

## Assumed knowledge

The following would be helpful to have:

- Basic knowledge of React
- Basic knowledge of React Router

## Getting Started

### Project Setup

Run the following command in a terminal:

```bash
yarn create vite granular-error-boundary --template react
cd granular-error-boundary
```

Now we can install the necessary dependencies:

```bash
yarn add react-router-dom
```

### Build the Components

The first thing we're going to do is create a component that will contain our application's `<Outlet />`, to be used as a layout. The aim is that if an error occurs, navigation should not be compromised.

```js
// @src/components/Layout.jsx
import { Link, Outlet } from "react-router-dom";

const Layout = () => (
  <div>
    <ul>
      <li>
        <Link to="/">Home</Link>
      </li>
      <li>
        <Link to="/other">Other</Link>
      </li>
    </ul>
    <Outlet />
  </div>
);

export default Layout;
```

Now the most important part, we need to create a component that catches errors that may happen in components. Let's create a simple retry mechanism along with a fallback ui.

```js
// @src/components/GranularErrorBoundary.jsx
import React from "react";

export class GranularErrorBoundary extends React.Component {
  state = { error: null };
  static getDerivedStateFromError(error) {
    return { error };
  }

  componentDidCatch() {
    this.props.onError?.(this.state.error);
  }

  tryAgain = () => this.setState({ error: null });

  render() {
    const fallbackProps = { error: this.state.error, tryAgain: this.tryAgain };
    const { fallback, children } = this.props;

    if (!fallback) {
      throw new Error("Granular Error Boundary requires a fallback prop");
    }

    if (this.state.error && typeof fallback === "function") {
      return fallback(fallbackProps);
    }

    return children;
  }
}
```

Taking into account the code block above, two lifecycle methods were used, `getDerivedStateFromError()` and `componentDidCatch()`.

`getDerivedStateFromError()` renders the fallback ui as soon as an error occurs. While in `componentDidCatch()` we invoke a prop called `onError()` which can be used to log or "trace" the error.

Something important to note is that the fallback is a callback, to which we pass some properties in the arguments, such as the error object and the retry method. This way we can have custom elements and consume those same properties.

### Build the Pages

With the main points concluded, we can now move on to building our application pages, starting with a simple `Home.jsx` page:

```js
// @src/pages/Home.jsx
const Home = () => {
  return (
    <div>
      <h1>Home page</h1>
    </div>
  );
};

export default Home;
```

Next, on the next page let's go step by step, first let's create the custom fallback of our error boundary that will receive props the properties of the fallback callback, in this example we will only look for the `tryAgain()` method.

```js
// @src/pages/Other.jsx
import { useCallback, useEffect } from "react";

import { GranularErrorBoundary } from "../components/GranularErrorBoundary";

const CustomErrorBoundary = ({ tryAgain }) => {
  return (
    <div>
      <h3>Custom Error Boundary</h3>
      <p>If you see this component, an error has occurred.</p>
      <button onClick={tryAgain}>Retry</button>
    </div>
  );
};

// ...
```

Then let's create a component with a single purpose, generate a random number within a specific range and if it is greater than 4, throw an error.

```js
// @src/pages/Other.jsx
import { useCallback, useEffect } from "react";

import { GranularErrorBoundary } from "../components/GranularErrorBoundary";

// ...

const ComponentToThrowError = () => {
  const randomIntFromInterval = useCallback((min, max) => {
    return Math.floor(Math.random() * (max - min + 1) + min);
  }, []);

  useEffect(() => {
    const num = randomIntFromInterval(1, 5);
    if (num > 4) {
      throw new Error("SOME RANDOM ERROR");
    }
  }, [randomIntFromInterval]);

  return <p>The component has been mounted successfully</p>;
};

// ...
```

Finally, we proceed to create the `Other.jsx` page component in which we will use the previously created components and make use of the granular error boundary. Taking into account the following composition:

```js
// @src/pages/Other.jsx
import { useCallback, useEffect } from "react";

import { GranularErrorBoundary } from "../components/GranularErrorBoundary";

// ...

const Other = () => {
  const handleOnError = useCallback((catchedError) => {
    console.error(`[Granular Error Boundary]: ${catchedError}`);
  }, []);

  return (
    <div>
      <h1>This is the "Other" Page</h1>
      <GranularErrorBoundary
        fallback={(fallbackProps) => <CustomErrorBoundary {...fallbackProps} />}
        onError={handleOnError}
      >
        <ComponentToThrowError />
      </GranularErrorBoundary>
    </div>
  );
};

export default Other;
```

### Router Setup

We already have the pages and components we need for today's example, the only thing left is to register the routes in the router.

```js
// @src/App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider,
} from "react-router-dom";

import HomePage from "./pages/Home";
import OtherPage from "./pages/Other";

import Layout from "./components/Layout";

const routesFromElements = createRoutesFromElements(
  <Route element={<Layout />}>
    <Route index element={<HomePage />} />
    <Route path="/other" element={<OtherPage />} />
  </Route>
);

const router = createBrowserRouter(routesFromElements);

export const App = () => {
  return <RouterProvider router={router} />;
};
```

If you followed the article step by step, you should have an end result similar to the following:

![image](https://res.cloudinary.com/dj5iihhqv/image/upload/v1668546204/2022-11-15_at_21.01.09_-_Peach_Opossum_oh5tyv.gif)

## Conclusion

As usual, I hope you enjoyed the article and that it helped you with an existing project or simply wanted to try it out.

If you found a mistake in the article, please let me know in the comments so I can correct it. Before finishing, if you want to access the source code of this article, I leave [here](https://github.com/FranciscoMendes10866/react-granular-error-boundary) the link to the github repository.