22 Mar 2023 · Software Engineering

    Building an Error Handling Layer in React

    13 min read
    Contents

    As with any other technology, React applications can involve errors. By default, React responds to a fatal error with a blank page. This is not ideal, because users have no way to understand what happened and why. This is a problem for developers because poor error handling affects user experience and the reputation of your application. Fortunately, you can fix this by adding an error handling layer to your React application.

    An error handling layer allows you to catch errors and display clear and helpful error messages to the user. This helps you avoid undesired crashes and improve the overall user experience as a result. Centralizing all error handling logic in one place provides several benefits and mitigates the problem mentioned earlier. This is what a React error handling layer is all about!

    Let’s learn what an error handling layer is, understand why your React app should have one, and see two approaches to building an error handling layer in React.

    What is an Error Handling Layer in React?

    In React, an error handling layer is the part of your frontend architecture that is responsible for detecting and handling errors that occur in your application. In other terms, it encapsulates and centralizes error handling logic in one place.

    A React error handling layer generally takes care of:

    • Error detection: the layer intercepts errors that occur during the execution of the frontend application.
    • Error handling: after detecting an error, the layer handles it gracefully. In particular, it prevents the error from crashing your application and presents a custom error page to users instead. Behind the scenes, the layer can also log the error or report it to an error monitoring service.

    By adding an error management layer to your React architecture, you can ensure that errors occurring in your application will be handled effectively and consistently. These are just some of the benefits that an error management layer can bring to your application. Read on to learn more.

    Why should your React App have an Error Handling Layer?

    As mentioned earlier, React has no built-in mechanism to recover from errors happening during rendering. When an error occurs, the only option React has is to unmount the entire application. As a result, users will see a blank screen. This is how React handles errors by default and, as you can imagine, this is far from an ideal solution.

    With an error handling layer, you can override the default behavior and avoid this. In detail, an error handling layer allows you to:

    • Avoid crashes: in case of an error, the layer intercepts it and allows you to handle the error as you want. This means preventing your application from crashing due to unhandled errors.
    • Improve user experience: whenever the layer detects an error, it displays a nice error page to the end user. Users tend to prefer using an application that provides useful error messages to one that crashes.
    • Centralize error handling logic: the layer stores the entire error handling logic of the application in the same place. This makes it easier to deal with errors consistently and integrate error logging or monitoring into your application. You can then use this collected information to identify the root cause of errors, debug, and fix them.

    These are all good reasons why your React app should have an error handling layer. Keep reading and see two approaches that you can follow to add an error handling layer to your frontend React architecture.

    Error Handling Layer with Error Boundaries in React

    Error boundaries have been the official way to handle errors since React 16, which was released in 2017. Specifically, error boundaries are React components that can catch JavaScript errors occurring anywhere in their child component tree. They allow you to display a fallback UI component when an error occurs, which handles the error gracefully and avoids a fatal crash.

    Note that standard error boundaries catch errors only during rendering, in lifecycle methods, and in component constructors. Thus, if a component within these boundaries throws an error, the error boundary will intercept it. Otherwise, it will ignore the error.

    Let’s now find out how to build and use an error boundary in React to implement an error handling layer!

    Creating an Error Boundary Component

    To create an error boundary in React, you need to define a class component that involves:

    • A state variable hasError: used for determining whether the error boundary has intercepted an error.
    • A static method called getDerivedStateFromError(): a React lifecycle method that is invoked after a descendant component throws an error. The value returned by the method updates the state of the error boundary component.
    • A componentDidCatch() method: a special React lifecycle method that is called when the error boundary catches an error. You can use it for logging the error or reporting it to an application performance management system.
    • A render() method with if-else logic`: in case of an error, it returns the fallback UI component. Otherwise, it displays the child components wrapped by the error boundary.

    Note that React supports the key lifecycle methods getDerivedStateFromError() and componentDidCatch() only on class components. This means that an error boundary cannot be a functional component.

    Here is what an error boundary class component should look like:

    import React from "react";
    import ErrorPage from "./ErrorPage";
    
    export default class StandardErrorBoundary extends React.Component {
        constructor(props) {
            super(props);
    
            // to keep track of when an error occurs
            // and the error itself
            this.state = {
                hasError: false,
                error: undefined
            };
        }
    
        // update the component state when an error occurs
        static getDerivedStateFromError(error) {
            // specify that the error boundary has caught an error
            return {
                hasError: true,
                error: error
            };
        }
    
        // defines what to do when an error gets caught
        componentDidCatch(error, errorInfo) {
            // log the error
            console.log("Error caught!");
            console.error(error);
            console.error(errorInfo);
    
            // record the error in an APM tool...
        }
    
        render() {
            // if an error occurred
            if (this.state.hasError) {
                return <ErrorPage />;
            } else {
                // default behavior
                return this.props.children;
            }
        }
    }

    When an error occurs in any of its child components, StandardErrorBoundary intercepts it, logs it, sets hasError to true, and renders the <ErrorPage /> fallback UI component.

    This is what ErrorPage might look like:

    export default function ErrorPage(props) {
        return (
            <div className={"error-page"}>
                <div className={"oops"}>Oops!</div>
                <div className={"message"}>Something went wrong...</div>
            </div>
        );
    }

    You can customize and style your fallback UI component as you like. An effective ErrorPage component should inform users of what happened, without going into too much detail or worrying them.

    You now know how to define an error bounding component in React! All that remains is to see how to use it.

    Using Your Error Boundary

    To implement an error handling layer in React, simply wrap your top-level app component with StandardErrorBoundary:

    <StandardErrorBoundary>
      <MyApp />
    </StandardErrorBoundary>

    From now on, any React-related error occurring in the app will be intercepted and handled by the error boundary.

    Keep in mind that a React app can have multiple error boundaries. Each of them will take care of handling errors in different component subtrees. However, to centralize the error logic in one place, you should use only one top-level error boundary.

    Limitations of this approach

    Error boundaries are a powerful tool but also come with several limitations you should take into account. Let’s dig into them.

    As mentioned in the official documentation, error boundaries cannot catch errors in:

    In other words, an error boundary can only handle errors thrown during React’s lifecycles.

    Also, error boundaries must be class components and do not support hooks.

    These limitations can easily pose a problem for your React application, especially considering that JavaScript relies heavily on event callbacks and async code. This is why we need to explore a different approach to error handling in modern React applications.

    Error Handling Layer with react-error-boundary

    react-error-boundary is an npm React library that provides an easy and reliable way to handle errors with error boundaries. It simplifies the process of creating error boundaries and offers effective solutions to overcome the limitations of a standard error boundary.

    Simply put, react-error-boundary greatly simplifies error handling in React. Add it to your project’s dependencies with:

    npm install react-error-boundary

    Next, keep following the tutorial and learn how to build an error handling layer in React with react-error-boundary.

    Getting started with the ErrorBoundary Component

    react-error-boundary exposes an ErrorBoundary component, which supports several props to help you build an error boundary component with no effort.This is what an error boundary created with react-error-boundary may look like:

    import ErrorPage from "./ErrorPage";
    import { ErrorBoundary } from "react-error-boundary";
    
    export default function ReactErrorBoundary(props) {
        return (
            <ErrorBoundary
                FallbackComponent={ErrorPage}
                onError={(error, errorInfo) => {
                    // log the error
    		console.log("Error caught!");  
    		console.error(error);  
    		console.error(errorInfo);
    		
    		// record the error in an APM tool...
                }}
            >
                {props.children}
            </ErrorBoundary>
        );
    }

    The FallbackComponent prop contains the fallback UI component to render in the event of an error. When this happens, ErrorBoundary invokes the callback function passed to onError. Otherwise, if no error occurs, ErrorBoundary renders the child components that it wraps. In other terms, this snippet does exactly what the error boundary defined previously does, but with less and more readable code.

    Just as we did earlier, wrap your React top-level component with ReactErrorBoundary to centralize the error handling logic:

    <ReactErrorBoundary>
      <MyApp />
    </ReactErrorBoundary>

    Do not forget that an error boundary component built with react-error-boundary can be either a functional or a class component. You are no longer limited to class components, and it is up to you to make the best decision according to your goals and preferences.

    Resetting your app after an Error

    react-error-boundary also provides a way to recover from errors caught by the error boundary component. Specifically, the onReset prop accepts a callback function that ErrorBoundary automatically passes to the FallbackComponent.

    Update your ReactErrorBoundary component as shown below:

    import ErrorPage from "./ErrorPage";
    import { ErrorBoundary } from "react-error-boundary";
    
    export default function ReactErrorBoundary(props) {
        return (
            <ErrorBoundary
                FallbackComponent={ErrorPage}
                onError={(error, errorInfo) => {
                    // log the error
    				console.log("Error caught!");  
    				console.error(error);  
    				console.error(errorInfo);
                }}
                onReset={() => {
                    // reloading the page to restore the initial state
                    // of the current page
                    console.log("reloading the page...");
                    window.location.reload();
    
                    // other reset logic...
                }}
            >
                {props.children}
            </ErrorBoundary>
        );
    }

    What the onReset callback function does is to simply reload the current page via JavaScript. This is the most basic error reset logic possible. In more complex applications, you may need to restore the state of the app to a previous point or update the Redux state.

    By default, ErrorBoundary passes to the FallbackComponent the following two props:

    • error: stores the error object caught by the error boundary.
    • resetErrorBoundary: contains the callback function passed to the onReset prop, if defined.

    So, you can now add a retry button to the ErrorPage as shown below:

    import "./ErrorPage.css";
    export default function ErrorPage(props) {
        return (
            <div className={"error-page"}>
                <div className={"oops"}>Oops!</div>
                <div className={"message"}>Something went wrong...</div>
                {props.resetErrorBoundary && (
                    <div>
                        <button className={"retry-button"} onClick={props.resetErrorBoundary}>
                            🔄 Try Again!
                        </button>
                    </div>
                )}
            </div>
        );
    }

    The fallback page shown in case of an error will now have a “Try Again!” button. When the user clicks on it, the onReset function will be executed and the current page reloaded. If the error is not in the initial rendering, but arose as a result of a particular interaction, the application will return to working as expected.

    Great! You can now recover from errors!

    Handling Errors in Async Code

    Another great feature offered by react-error-boundary is the useErrorHandler() hook. This allows you to catch errors that would not otherwise be caught by a traditional React error boundary. This means that you can take advantage of useErrorHandler() to intercept errors during API requests, event handlers, and more.

    Suppose that one of the pages in your React application relies on a crucial API call. When this API fails, you want to raise an error and handle it in the error layer. Since API calls involve asynchronous code, you cannot do that with a standard error boundary. Thanks to useErrorhandler(), you can achieve the desired result with:

    import { useEffect } from "react";
    import { useErrorHandler } from "react-error-boundary";
    
    export default function MyPageComponent(props) {
        const handleError = useErrorHandler();
    
        useEffect(() => {
          // API call
          fetchSomeData().then(() => {
              // ...
          }).catch((e) => {
              // propagate the error to the error boundary
              handleError(e);
          })
        }, [])
    
        // return ...
    }

    Similarly, you can adopt useErrorhandler() in other situations where standard error boundaries fall short. Also, keep in mind that useErrorhandler() propagates the error to any error boundary, regardless of whether it is implemented with react-error-boundary or with the standard approach.

    Et voilà! Your React error handling layer is more powerful than ever!

    Error Handling Layer in action

    You can play with the live demo React application below to see how the two approaches behave differently:

    When you disable the error handling layer, the first button will result in a blank page. Do not forget that this is how React natively handles errors. On the other hand, the async error will be logged but nothing more will happen.

    If you use the error handling layer with a standard error boundary, the first button will lead to the error page. Yet, the async error will not be intercepted as desired.

    With the react-error-boundary error layer enabled, both sync and async errors will be intercepted and handled as desired. Also, when clicking the “Try Again!” button, the page will reload and the application will recover from the error.

    If you want to explore the code of the demo application or test it locally, clone the repository that supports this article with:

    git clone "https://github.com/Tonel/error-handling-layer-react"

    Then, launch the React application on your machine with:

    cd "error-handling-layer-react"
    npm install
    npm run start

    Great! You now know everything you need to know about handling errors in React!

    Conclusion

    In this tutorial, you learned what an error handling layer is, its essential benefits in the frontend world, and how to integrate it into a React application. An error handling layer is nothing more than an error boundary component that catches and handles errors gracefully. This allows you to consolidate error handling logic into a single layer of the React application architecture, making the debugging easier. As shown in a complete example, implementing an error handling layer in React is simple and takes only a few minutes.




    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    I'm a software engineer, but I prefer to call myself a Technology Bishop. Spreading knowledge through writing is my mission.
    Avatar
    Reviewed by:
    I picked up most of my soft/hardware troubleshooting skills in the US Army. A decade of Java development drove me to operations, scaling infrastructure to cope with the thundering herd. Engineering coach and CTO of Teleclinic.