Skip to content

Building Resilient React Apps

UIs that are heavily dependent on javascript for rendering views can be prone to crashing when uncaught errors are thrown. React-based applications are no exception. This post aims to provide guidance for building resilient user interfaces by prioritizing error handling with React.  

Error Classifications

I like thinking about the types of errors that can happen in the apps I build in two ways, non-fatal and fatal.

Non-fatal errors are situations where the UI can be partially recovered. In this scenario, users can still navigate to other pages or interact with other content on the page not impacted by the error. Ideally, these errors are handled in a graceful way with adequate messaging and ultimately, it means users or operators do not have to do anything. For example, an external API or dependency could go down and will eventually resolve itself.

Fatal errors are non-recoverable UI states where the only “fix” is having users kindly refresh the page and not hit that path again or, worse case, an entire rollback of the code because the UI is not useable. I’d love to say this should never happen, but I’d be lying. Without proper testing in place, javascript builds can be corrupted, SHAs for subresource integrity could be invalid, or some other link in the chain of what puts it all together could be broken. It’s important to have sufficient monitoring and telemetry in place to alert when this situation arises.

Building resilient UIs means setting intentional boundaries to herd the errors in your app from being classified as fatal to non-fatal because stuff happens. Intentionally writing code in a way that makes these recoverable helps reduce risk and keep things running.

Creating (Error) Boundaries

First, React tooling supports a concept of Error boundaries to help catch errors. Error boundaries are explicit React components built in a way to catch errors anywhere in their child component tree to enable you to report those errors and display a fallback UI instead of something fatal.

Here’s an example from the React docs:

class ErrorBoundary 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.
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    // Example "componentStack":
    //   in ComponentThatThrows (created by App)
    //   in ErrorBoundary (created by App)
    //   in div (created by App)
    //   in App
    logErrorToMyService(error, info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return this.props.fallback;
    }

    return this.props.children;
  }
}

Then you can wrap a part of your component tree with it and the fallback view to present when an error occurs:

<ErrorBoundary fallback={<p>Something went wrong</p>}>
  <App/>
</ErrorBoundary>

Where to place Error Boundaries?

“The granularity of error boundaries is up to you”.

React Docs

That’s not super helpful. A better way to think about this is that the placement of error boundaries in the component tree determines whether or not advanced error handling needs to happen.

If you could only spend time and effort to put a single error boundary in your app, I’d say throw it at the root of your component tree like in the example above. The placement of the ErrorBoundary in the component tree determines whether or not advanced error handling needs to happen.

Does this prevent a fatal error state? No. But it does allow you to gracefully handle fatal errors and present some form of messaging to end users. To me, this is the bare minimum any front-end app should have.

If you have more time and resources to spare, it would be helpful to first break down your UI layout into sections to understand the exact granularity of error boundaries. What content is mostly static? What changes often, either due to routing and/or data context?

A simple approach to this could just be a navigation section and a main content section.

In this scenario, it would be wise to place an error boundary around the main content section. If a subroute or component inside breaks, users can still use the navigation to view other content.

As for the navigation section, it might not need an error boundary, but it could be helpful to place an alert banner for things like planned maintenance or unplanned downtime.

For more advanced and granular approaches, consider the different UI states that can exist like loading, first-time experience, data exists, no data exists, and error responses. Are you confident that error handling is taken care of? Otherwise, consider wrapping the parent container, card, table, or page with an error boundary if needed.

Robust apps take time, so consider prioritizing work based on the error classifications mentioned and then iterate as needed. By having a healthy mix of UI state management and error boundaries, you’ll spend less time putting out fires and more time shipping features.

Comments

Add a comment

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

Name
Website