Catch errors in React with Error Boundaries

created at 06-14-2022 views: 7

In a React project, what's the best way to handle errors to ensure your application is resilient, continuously responsive, and provides the best user experience? That's inseparable from using Error Boundary.

What is Error Boundary?

Error boundaries are React's way of handling application errors. It makes it possible to react, display errors, report them to an error reporting service, recover from runtime errors, and provide an alternate user interface when applied.

Error boundaries were introduced in React 16 and are the only components that need to be written as class components (so no hooks yet!), but should definitely be part of any modern React application. Typically, it is sufficient to create a single error boundary at the root level of the entire application.

Error boundaries are regular class components that implement one (or both) of the following methods:

  • static getDerivedStateFromError(error): Returns a new state based on the caught error. A status flag is usually given to tell the error boundary whether to provide a fallback UI.
  • componentDidCatch(error, errorInfo): This method is called whenever an error occurs. Errors (and any extra information) can be logged to the error reporting service, attempts to recover from errors, and whatever else needs to be done.

The Error Boundary component with the above two basic functions:

class ErrorBoundary extends React.Component {
   state = { hasError: false };
   static getDerivedStateFromError(error) {
     return { hasError: true };
   }
   componentDidCatch(error, errorInfo) {
     errorService.log({ error, errorInfo });
   }
   render() {
     if (this.state.hasError) {
         return (
             <div>
                 <h1>Oops, we screwed up</h1>
                 <button type="button" onClick={() => this.setState({ hasError: false })}>
                 try again?
                 </button>
             </div>
             );
     }
     return this.props.children;
   }
}

how to use:

ReactDOM.render(
    <ErrorBoundary>
        <App />
    </ErrorBoundary>,
    document.getElementById('root')
)

Better Error Boundary

Error bounds are great for catching unexpected runtime errors during rendering. However, there are several types of errors that are not caught and need to be handled differently. These include:

  • Errors in event handlers (for example, when a button is clicked)
  • Errors in async callbacks (eg setTimeout)
  • The error occurred in the error boundary component itself
  • Errors that occurred during server-side rendering

These limitations may sound severe, but most of the time they can be resolved by using try-catch and similar hasError states.

function SignUpButton(props) {
     const [hasError, setError] = React.useState(false);
     const handleClick = async() => {
         try {
             await api.signUp();
         } catch(error) {
             errorService.log({ error })
             setError(true);
         }
     }
     if (hasError) {
         return <p>Login failed</p>;
     }
     return <button onClick={handleClick}>login</button>;
}

However, we can implement error handling logic that reuses error boundaries in event handlers and asynchronous locations via the context API:

export const ErrorBoundaryContext = React.createContext(() => {});

export const useErrorHandling = () => {
    return React.useContext(ErrorBoundaryContext)
}


class ErrorBoundary extends React.Component {
    state = { hasError: false };
    static getDerivedStateFromError(error) {
        return { hasError: true };
    }
    componentDidCatch(error, errorInfo) {
        errorService.log({ error, errorInfo });
    }
    triggerError = ({ error, errorInfo }) => {
        errorService.log({ error, errorInfo });
        this.setState({ hasError: true });
    }
    resetError = () => this.setState({ hasError: false });
    render() {
        return (
            <ErrorBoundaryContext.Provider value={this.triggerError}>
              {this.state.hasError
                ? <div>
                    <h1>Oops, we screwed up</h1>
                    <button type="button" onClick={() => this.setState({ hasError: false })}>Try again?</button>
                  </div>
                : this.props.children
              }
            </ErrorBoundaryContext.Provider>
        );
    }
}


function SignUpButton(props) {
    const { triggerError } = useErrorHandling();
    const handleClick = async() => {
        try {
            await api.signUp();
        } catch(error) {
            triggerError(error);
        }
    }
    return <button onClick={handleClick}>login</button>;
}

use react-error-boundary

Error boundary component [react-error-boundary] created by React Core team member Brian Vaughn, which provides almost the same capabilities as above, supporting passing in custom fallback components and reset logic.

ReactDOM.render(
    <ErrorBoundary 
        FallbackComponent={MyFallbackComponent}
        onError={(error, errorInfo) => errorService.log({ error, errorInfo })}
    >
        <App />
    </ErrorBoundary>,
    document.getElementById('root')
)

Either writing your own error boundary, or using the react-error-boundary library to make your application fail gracefully, or even include errors in some parts of the application while the rest continue to work, is desirable.

Open source session replay

OpenReplay is an open source session replay suite that sees what users are doing on web applications, helping to solve problems faster. OpenReplay is self-hosted and has full control over the data.

created at:06-14-2022
edited at: 06-14-2022: