In-Depth Tutorial for Mastering React Error Boundaries

Relia Software

Relia Software

Huy Nguyen

Relia Software

featured

React Error Boundaries are specialized components that can catch errors anywhere within their child components, acting as guardians in the component tree.

React Error Boundaries are specialized components that can catch errors anywhere within their child components

Table of Contents

Have you ever encountered a frustrating blank screen while using a web application? This can often be the result of unhandled errors within the application's code. In React applications, unhandled errors lead to the entire application crashing, leaving users confused and developers frustrated. Fortunately, there's a robust solution to this issue: React Error Boundaries.

This article delves into the world of React Error Boundaries, exploring their core functionalities and how they can significantly improve the user experience and overall stability of your React applications. We'll explore the key benefits of error boundaries, along with the technical details of how they catch errors and render fallback UI. Additionally, we'll provide practical guidance on implementing error boundaries within your React components.

>> Read more:

What are React Error Boundaries?

React Error Boundaries are specialized components that act as guardians within your component tree. They have the remarkable ability to catch errors that occur anywhere within their child component hierarchy. Imagine them as safety nets, preventing errors from propagating upwards and disrupting unrelated parts of your application.

Error boundaries can render a user-friendly fallback UI in place of the crashed component. This not only prevents a jarring user experience with a blank screen but also provides valuable clues about the error's location for debugging purposes.

By strategically incorporating error boundaries within your React components, you can significantly enhance the robustness and stability of your application. The following sections will delve deeper into the technical details of how error boundaries work, explore best practices for their implementation, and showcase their advantages in building exceptional React applications.

Benefits of Using React Error Boundaries

React applications, by default, lack a mechanism to handle errors during rendering. When an error happens, the only choice is to remove the whole app. This results in the entire application crashing and displaying a blank screen to the user. Fortunately, React Error Boundaries come to the rescue! Here are some typical benefits of Error Boundaries:

  • Enhanced User Experience: When an error occurs within an error boundary, you can display user-friendly error messages or fallback UI. This helps in communicating issues to users clearly and understandably, reducing frustration.
  • Streamlined Debugging: Error boundaries often log error information, providing valuable data for developers to pinpoint the root cause of the issue and speed up the debugging process.
  • Granular Control: You can strategically place error boundaries throughout your application. This allows for granular control over error handling, enabling you to isolate errors within specific components or implement boundaries at the application level.
  • Error Isolation: Error boundaries excel at containing errors within their designated boundaries. This means that an error in a specific component won't bring down the entire application. Other parts of your app can continue functioning seamlessly, preventing cascading failures.
  • Retry Mechanisms: While not a core functionality, some advanced error boundaries can even implement retry mechanisms, potentially allowing the application to recover from transient errors without user intervention.

How Does React Error Boundaries Work?

React Error Boundaries offer a superhero-like ability to catch errors within your component hierarchy. But how exactly do they work? Let's delve into the technical details:

Catching Errors Within the Tree

Error boundaries are constantly on the lookout for errors. They use special tools (lifecycle methods), specifically getDerivedStateFromError and componentDidCatch, to detect errors anywhere within their child components before they can cause an entire app crash.

  • getDerivedStateFromError (optional): This lifecycle method is invoked during the rendering phase if an error occurs. It receives the error object and the current state as arguments, and can optionally return an updated state object that will be used to trigger a re-render with the fallback UI.
  • componentDidCatch (required): This lifecycle method is invoked after the error has happened (but before the UI updates). It receives the error object and the information about the component that threw the error. This method doesn't directly update the UI, but it allows you to log the error details for debugging purposes or perform actions like sending error reports to external services.

Rendering the Fallback UI

When an error is caught by an error boundary, the default behavior is to transition the component into an error state. This state change, often triggered by getDerivedStateFromError, is what prompts the error boundary to render its fallback UI. This fallback UI is typically a custom component you define that provides a user-friendly message or alternative display in place of the crashed component.

Error Boundary Lifecycle Summary

  • Error occurs: An error is thrown within a component inside the error boundary's subtree.
  • getDerivedStateFromError (optional): If present, this method is invoked during rendering and can return a new state to trigger the fallback UI.
  • componentDidCatch (required): This method is invoked after the error, allowing you to log or report the error.
  • Fallback UI Renders: The error boundary transitions to its error state, rendering the custom fallback UI you've defined.

By understanding this error lifecycle and effectively utilizing getDerivedStateFromError and componentDidCatch, you can create robust error boundaries that gracefully handle errors within your React applications.

Implementing React Error Boundaries: A Practical Example

This repo will explain how to use react-error-boundary in React, you can find it in here.

First, clone the project

git clone https://github.com/nvkhuy/examples.git

Navigate to react-error-boundary example:

cd react-error-boundary

To Install:

npm install

To Run:

npm run dev

Project structure:

examples/react-error-boundaries/src
├── App.css
├── App.tsx
├── assets
│   └── react.svg
├── components
│   ├── CartItem.tsx
│   ├── CartItems.tsx
│   ├── CheckoutButton.tsx
│   ├── CheckoutPage.tsx
│   ├── CheckoutSummary.tsx
│   └── errors
│       ├── ErrorBoundrayComponent.tsx
│       └── ProductsFetchingError.tsx
├── errorHanlding
│   ├── usingErrorBoundaries.tsx
│   ├── usingState.tsx
│   └── usingTheRightWay.tsx
├── fetchers
│   └── products.ts
├── index.css
├── main.tsx
└── vite-env.d.ts

React Error Boundary implementation is on this file: errorHandling/usingErrorBoundaries.tsx

export class StandardErrorBoundary extends React.Component<any, any> {
  state: {
    hasError: boolean;
    error?: Error;
  };

  constructor(props: any) {
    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,
    };
  }

  // Log the error to some sort of a service logger
  componentDidCatch(error: any, errorInfo: any) {
    console.log("Error caught!");
    console.error(error);
    console.error(errorInfo);
  }

  render() {
    // if an error occurred
    if (this.state.hasError) {
      return <ProductsFetchingError error={this.state.error?.message || ""} />;
    } else {
      // default behavior
      return this.props.children;
    }
  }
}

Here’s a breakdown of the code:

  • state: This is the state of the component, which includes hasError (a boolean indicating if an error has occurred) and error (the error that occurred).
  • constructor(props: any): This is the constructor of the component, which initializes the state.
  • getDerivedStateFromError(error): This is a static method that updates the state so the next render will show the fallback UI. When an error is thrown, this lifecycle method is invoked with the error thrown as the argument.
  • componentDidCatch: This method is called after an error has been thrown by a descendant component. It’s used to log the error information for debugging purposes.
  • render(): This method returns the UI of the component. If an error occurred (this.state.hasError is true), it renders a ProductsFetchingError component with the error message. If no error occurred, it renders the children components normally (this.props.children).

Here the child component that React Error Boundary will wrap:

export const UsingErrorBoundaries = () => {
  const [cartItems, setCartItems] = useState([]);

  const handlePayClick = () => {
    // Handle the payment process here
  };

  const subtotal = cartItems.reduce(
    (sum, item: any) => sum + item.price * item.quantity,
    0
  );
  const discount = 0;
  const total = subtotal - discount;

  useEffect(() => {
    const fetchItems = async () => {
      try {
        const items = await fetchCartItems();
        setCartItems(items);
      } catch (err) {}
    };
    fetchItems();
  }, []);

  return (
    <div className="checkout-page min-h-screen py-8 px-4">
      <div className="max-w-2xl mx-auto">
        <h1 className="text-3xl font-bold text-white mb-6">Checkout</h1>
        <CartItems items={cartItems} />
        <CheckoutSummary
          subtotal={subtotal}
          discount={discount}
          total={total}
        />
        <div className="mt-4">
          <CheckoutButton onClick={handlePayClick} />
        </div>
      </div>
    </div>
  );
};

Here’s a breakdown of what it does:

  • State Initialization: It initializes a state variable cartItems with an empty array. This state will hold the items in the cart.
  • Payment Handler: It defines a function handlePayClick which will be called when the payment button is clicked. The actual payment process should be implemented inside this function.
  • Calculating Subtotal, Discount, and Total: It calculates the subtotal of the cart by reducing the cartItems array, summing up the product of the price and quantity of each item. The discount is set to 0, and the total is calculated as the difference between the subtotal and the discount.
  • Fetching Cart Items: It uses the useEffect hook to fetch the cart items when the component is mounted. The fetchItems function is defined as an asynchronous function inside the useEffect hook. It fetches the cart items using the fetchCartItems function (which is not defined in the provided code) and updates the cartItems state.
  • Rendering: It returns a JSX that represents the checkout page. This includes the title “Checkout”, a list of cart items, a summary of the checkout including the subtotal, discount, and total, and a checkout button that calls the handlePayClick function when clicked.

Here the result when you run npm run dev in terminal:

the result when you run npm run dev in terminal

The code below demonstrates how to intentionally throw an error within the CheckoutSummary.tsx component to simulate an error scenario. However, for robust testing, consider utilizing libraries or frameworks that simulate various error conditions (e.g., network errors, API failures).

const CheckoutSummary = ({ subtotal, discount, total }) => {
  // intend to throw errror
  throw new Error("Unexpcted Render Error occured!");

  return (
    <div className="checkout-summary p-4 bg-gray-900 border-2 border-slate-200 shadow-md rounded-md">
      <p className="text-lg font-semibold mb-2">Summary</p>
      <p className="text-sm text-gray-500">Subtotal: ${subtotal}</p>
      <p className="text-sm text-gray-500">Discount: ${discount}</p>
      <p className="text-lg font-semibold">Total: ${total}</p>
    </div>
  );
};

Here is where react error boundary is used. You can find it in App.tsx:

function App() {
    return (
        <div className="App">
            <StandardErrorBoundary>
                <UsingErrorBoundaries/>
            </StandardErrorBoundary>
        </div>
    );
}

To stop throwing error and see how app renders when CheckoutSummary succeeds, you comment like this:

const CheckoutSummary = ({ subtotal, discount, total }) => {
  // comment line below to have succeed checkout summary
  // throw new Error("Unexpcted Render Error occured!");

  return (
    <div className="checkout-summary p-4 bg-gray-900 border-2 border-slate-200 shadow-md rounded-md">
      <p className="text-lg font-semibold mb-2">Summary</p>
      <p className="text-sm text-gray-500">Subtotal: ${subtotal}</p>
      <p className="text-sm text-gray-500">Discount: ${discount}</p>
      <p className="text-lg font-semibold">Total: ${total}</p>
    </div>
  );
};

And here is the result when the app succeeds in rendering:

 the result when the app succeeds in rendering

>> You may consider: Mastering React Test Renderer for Streamlined Testing

Alternative Error Handling Approaches If Not Using React Error Boundary

Apart from using Error Boundary, there are other ways to handle errors in React. We will mention 3 typical ways in this part.

Try-Catch Blocks

You can use try-catch blocks to handle errors that occur within a component’s render method or during API calls. Here’s an example:

import React, { useState } from 'react';

const MyComponent = () => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  try {
    // make API call to fetch some data
    const response = await fetch('https://api.example.com/data');
    const json = await response.json();
    setData(json);
  } catch (error) {
    setError(error);
  }

  return (
    <div>
      {error ? <p>An error occurred: {error.message}</p> : null}
      {data ? <p>Data: {data}</p> : null}
    </div>
  );
}

The component makes an API call to fetch some data and stores the result in the data state variable. If there is an error during the API call, the error is caught by the catch block and stored in the error state variable.

However, they have limitations:

  • Scope: They only catch errors within the try block, not in asynchronous code (e.g., promises) or event handlers.
  • Propagation: Uncaught errors within the try block will still propagate up the component tree.

Logging Errors

Always log errors with meaningful messages and stack traces to aid in debugging. Use tools like the JavaScript console or third-party error monitoring services.

Testing

Implement unit tests, integration tests, and end-to-end tests to catch errors early in the development process.

Try-Catch vs. React Error Boundary: Detailed Comparison

We will compare between the try-catch method and React Error Boundaries using the aforementioned example:

Scope of Error Handling

  • Try-Catch: This method is useful for handling errors within a single component. It works well for imperative code, such as API calls.
  • Error Boundaries: These are designed to catch JavaScript errors anywhere in their child component tree. They work well for declarative code, such as rendering components.

Type of Errors Caught

  • Try-Catch: This method can catch errors that occur during the execution of synchronous code.
  • Error Boundaries: These catch errors during rendering, in lifecycle methods, and constructors of the whole tree below them. However, they do not catch errors for event handlers, asynchronous code, server-side rendering, and errors thrown in the error boundary itself.

Fallback UI

  • Try-Catch: You can handle the error and decide what to render based on the error.
  • Error Boundaries: These allow you to display a fallback UI in case of an error. This helps prevent the entire application from crashing and provides a better user experience.

Error Propagation

  • Try-Catch: If an error is thrown inside a try block and is not caught, it will propagate up the call stack.
  • Error Boundaries: These prevent unhandled errors from propagating and affecting the overall user experience. They can be set up around the entire app or individual components for more granular control.

While try-catch blocks are great for handling errors in imperative code, React Error Boundaries are specifically designed to handle errors in React’s declarative code and provide a better user experience by displaying a fallback UI when an error occurs.

Limitations of React Error Boundaries

While React Error Boundaries offer a powerful error handling mechanism, they have some limitations to be aware of:

Limited Scope

Error Boundaries can only catch errors that occur within their child component tree. This means:

  • Errors originating from the error boundary component itself are not caught.
  • Errors thrown within event handlers of child components are not captured.

Asynchronous Code Challenges

Error Boundaries cannot directly handle errors thrown within asynchronous code like promises or functions using setTimeout. You'll need to implement separate error handling mechanisms within the asynchronous code itself, such as try-catch blocks or .catch() methods on promises.

Server-Side Rendering (SSR) Considerations

Errors encountered during SSR (Server-Side Rendering) won't be caught by Error Boundaries. You need to implement separate error handling mechanisms specifically for SSR to ensure a smooth user experience on the initial page load.

Debugging Difficulties

While componentDidCatch allows logging errors, debugging errors within the error boundary itself can be challenging. The error might originate from a deeply nested component within the tree, making it difficult to pinpoint the exact source.

Limited User Feedback

The default behavior of Error Boundaries is to display a generic fallback UI when an error occurs. Consider providing more informative error messages or user guidance within the fallback UI for a better user experience.

>> Read more about React:

Final Thoughts

Whether you're using class-based or functional components, react-error-boundary offers a versatile toolkit. It provides components, advanced components, and custom Hooks, enabling you to implement robust error handling strategies within your React components. Additionally, it supports custom fallback UIs, error reset features, and error reporting to make sure a seamless user experience even when errors arise.

Integrating react-error-boundary into your React application strengthens error handling, simplifies debugging, and enhances the final product. By leveraging this tool, you can minimize time spent managing errors and focus on what truly matters - crafting exceptional features for your users.

>>> Follow and Contact Relia Software for more information!

  • coding
  • development