DEV Community

Cover image for React Error Handling Made Easy with Component-Based Strategies
Francisco Mendes
Francisco Mendes

Posted on • Edited on

React Error Handling Made Easy with Component-Based Strategies

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:

yarn create vite granular-error-boundary --template react
cd granular-error-boundary
Enter fullscreen mode Exit fullscreen mode

Now we can install the necessary dependencies:

yarn add react-router-dom
Enter fullscreen mode Exit fullscreen mode

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.

// @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;
Enter fullscreen mode Exit fullscreen mode

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.

// @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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

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

export default Home;
Enter fullscreen mode Exit fullscreen mode

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.

// @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>
  );
};

// ...
Enter fullscreen mode Exit fullscreen mode

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.

// @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>;
};

// ...
Enter fullscreen mode Exit fullscreen mode

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:

// @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;
Enter fullscreen mode Exit fullscreen mode

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.

// @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} />;
};
Enter fullscreen mode Exit fullscreen mode

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

image

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 the link to the github repository.

Top comments (0)