Skip to main content

Command Palette

Search for a command to run...

Safety Net of React: Understanding and Implementing Error Boundaries

Error-Proof React: A Deep Dive into Error Boundaries for Uninterrupted User Experiences

Updated
•6 min read•View as Markdown
Safety Net of React: Understanding and Implementing Error Boundaries

Introduction:

React has emerged as one of the most popular and widely used JavaScript libraries for building dynamic and interactive user interfaces. With its component-based architecture and declarative nature, React simplifies the process of building complex UIs. However, as powerful as React may be, it cannot guarantee a foolproof web experience. Errors can still occur, causing the app to crash and leaving users frustrated. For that, React introduces a powerful feature called Error Boundaries to handle these mishaps gracefully.

This blog is also available on my Medium but with a better user interface.

What Exactly Are Error Boundaries?

So, what exactly are error boundaries? Error boundaries are React components that wrap around other components to catch errors that occur during rendering. When an error occurs in any component wrapped by an error boundary, it prevents the entire application from crashing. Instead of an error, the error boundary catches the error within its child components, allowing you to gracefully handle the issue without crashing the entire app. By encapsulating the error within the boundary, React prevents the entire application from crashing, maintaining a seamless user experience.

Why do we need it?

Error boundaries in React are like safety nets for your app. They catch errors in specific parts of your code, preventing the whole app from crashing. With error boundaries, you can control how to handle errors, like showing a friendly message instead of a confusing crash.

These are a couple of points but in easy terms:

  • Errors in React components can crash the whole app, but error boundaries stop this from happening.

  • Even if one part of the app has an error, error boundaries keep the rest working, making the app more stable.

  • Crashes are bad for user experience, but error boundaries help show friendly messages or backup UIs.

  • Error boundaries let developers deal with errors in specific parts of the app, making it easier to find and fix issues.

  • Developers can catch and log errors easily with error boundaries, helping them find and fix problems more easily.

Step-By-Step Integration

Let us have a step-by-step implementation of the error boundary.

  1. Let us start with creating a Class component - ErrorBoundaryWrapper.jsx and let us initialize a hasError state with default value as false

     import React from "react"
    
     export default class ErrorBoundaryWrapper extends React.Component {
       constructor(props) {
         super(props);
         this.state = { hasError: false };
       }
    
       render() {
           return this.props.children;
       }
     }
    
  2. Now Let us add a method, getDerivedStateFromError which is a static method in class component that gets called when an error is thrown in a descendant component of the error boundary.

     static getDerivedStateFromError() {
       return { hasError: true };
     }
    
  3. Let us add one more lifecycle method, componentDidCatch which is called after an error has been thrown by a descendant component. It provides an opportunity to perform side effects, such as logging the error.

     componentDidCatch(error, errorInfo) {
         // Other logic 
         console.log(error, errorInfo);
     }
    
  4. After adding the getDerivedStateFromError, let us update our render() in such a way that it throws a fallback UI when there is an error.

     render() {
         if (this.state.hasError) {
           // Render custom fallback UI
           return <span style={{ marginRight: "1rem" }}>Something went wrong.</span>;
         }
    
         return this.props.children;
       }
    
  5. Our final ErrorBoundaryWrapper.jsx file should look something like this

     import React from "react"
    
     export default class ErrorBoundaryWrapper 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.
         console.log({ error })
         return { hasError: true };
       }
    
       componentDidCatch(error, errorInfo) {
         // Log the error or other logic (make some API request to your backend regarding error)
         console.log(error, errorInfo);
       }
    
       render() {
         if (this.state.hasError) {
           // Render custom fallback UI
           return <span style={{ marginRight: "1rem" }}>Something went wrong.</span>;
         }
    
         return this.props.children;
       }
     }
    
  6. Now, Let us Modify the App.jsx to manually throw an error when a user presses a button 3 times.

     import { Fragment, useEffect, useState } from 'react'
    
     function ErrorButton() {
       const [count, setCount] = useState(3)
    
       const handleClick = () => {
         setCount(prev => prev - 1)
       }
    
       useEffect(() => {
         if (count === 0) {
           throw new Error("Error!")
         }
       }, [count])
    
       return (
         <button onClick={handleClick}>
           Click me {count} times to throw Error!
         </button>
       )
     }
    
     export default function App() {
       return (
         <Fragment>
           <ErrorButton />
         </Fragment>
       )
     }
    
  7. Now let us add some styling for the button in App.css

     button {
       margin-right: 1rem;
       background: #0074d9;
       box-shadow: none;
       border: none;
       cursor: pointer;
       padding: 1rem 2rem;
       color: #fff;
     }
    
  8. Now let us add 3 sections to our App component where

    • The first section will have no error boundary,

    • The second will have an error boundary but it will be wrapped inside a parent component

    • Finally, the third section will have an error boundary at the component level for each button.

Finally, it should look something like this,

    import { Fragment, useEffect, useState } from 'react'
    import './App.css'
    import ErrorBoundaryWrapper from './ErrorBoundaryWrapper'

    function ErrorButton() {
      const [count, setCount] = useState(3)

      const handleClick = () => {
        setCount(prev => prev - 1)
      }

      useEffect(() => {
        if (count === 0) {
          throw new Error("Error!")
        }
      }, [count])

      return (
        <button onClick={handleClick}>
          Click me {count} times to throw Error!
        </button>
      )
    }

    export default function App() {
      return (
        <Fragment>
          <h3>No error boundary</h3>
          <ErrorButton />
          <hr />
          <h3>Error boundary at parent level</h3>
          <ErrorBoundaryWrapper>
            <div>
              <ErrorButton />
              <ErrorButton />
            </div>
          </ErrorBoundaryWrapper>
          <hr />
          <h3>Error boundary at component level</h3>
          <ErrorBoundaryWrapper>
            <ErrorButton />
          </ErrorBoundaryWrapper>
          <ErrorBoundaryWrapper>
            <ErrorButton />
          </ErrorBoundaryWrapper>
        </Fragment>
      )
    }
  1. Below is the Replit for the same.

    %[https://replit.com/@p03goswami/Error-Boundary-React]

By playing with the above example, you will notice that

  • when you trigger an error at Level 1, it will crash the entire app.

    • At level 2, an error in any button will replace the component with a fallback UI that is under the parent.
  • For the third one, while an error, we will see a replaced fallback UI with the component itself.

This is the working of error boundaries in React. You can control which fallback UI you want to display when a certain error occurs in their children component.

Pitfalls/Limitations of Error Boundaries

So far, we have just discussed the advantages of Error boundaries. But not to forget, there are some drawbacks to consider as well. Such as,

  • Error boundaries can only catch errors that occur during the synchronous rendering phase or lifecycle methods.

  • They cannot catch errors in asynchronous operations like setTimeout or event handlers such as onClick.

  • The use of error boundaries can introduce coupling between components, as you need to wrap components with error boundaries to catch errors in their descendants.

So, error boundaries in React have limitations—they can't catch asynchronous errors like those in setTimeout or onClick handlers. Additionally, their use introduces component coupling, impacting modularity. Developers should be aware of these constraints and explore alternative strategies for handling asynchronous errors and minimizing coupling in certain scenarios.

Conclusion

In summary, React's Error Boundaries are invaluable for enhancing application stability. They act as trusty safety nets, giving developers the power to manage errors in specific components and keep the user experience smooth. The step-by-step guide we've walked through shows how easy it is to add it to your app.

But, hey, let's not forget they're not invincible. They might struggle with asynchronous errors and bring in a bit of component coupling drama. So, while Error Boundaries are the cool sidekicks in your toolkit, it's wise to team them up with other strategies like testing and monitoring for an all-around reliable and user-friendly user experience.

That is it, folks! Keep coding with confidence, and may your apps be crash-free! 🚀