React Error Boundaries: Revisited

Published See discussion on Bluesky

First off - if you haven't already I recommend giving my old React Error Boundaries blog post a read through, that post covers the basics of error boundaries. Once you've read it, jump back here to learn more!

Since I last published that blog post, there have been quite a few advancements in and around React, including but not limited to React Server Components!

Sadly however, we still need to use class components for error boundaries at the time of writing. Who knows, maybe in another 5 years this will have finally changed?!

My Go To Implementation

In most of my projects I reach for the following Error Boundary implementation, it offers a decent amount of flexibility where I want it, but also isn't too over the top.

1"use client";
2import {Component} from "react";
3import type {ComponentType, ReactNode} from "react";
4
5type Props = {
6 children: ReactNode;
7 fallback: ComponentType<{
8 reset(): void;
9 error: Error
10 }>;
11}
12
13type State = {
14 error: Error | null;
15}
16
17class ErrorBoundary extends Component<Props, State> {
18 state: State = {
19 error: null
20 };
21
22 static getDerivedStateFromError(error: Error) {
23 return {error};
24 }
25
26 reset = () => {
27 this.setState({error: null});
28 }
29
30 render() {
31 let {error} = this.state;
32 let {children, fallback: Fallback} = this.props;
33
34 if (error) {
35 return <Fallback reset={this.reset} error={error} />;
36 }
37
38 return children;
39 }
40}
1"use client";
2import {Component} from "react";
3import type {ComponentType, ReactNode} from "react";
4
5type Props = {
6 children: ReactNode;
7 fallback: ComponentType<{
8 reset(): void;
9 error: Error
10 }>;
11}
12
13type State = {
14 error: Error | null;
15}
16
17class ErrorBoundary extends Component<Props, State> {
18 state: State = {
19 error: null
20 };
21
22 static getDerivedStateFromError(error: Error) {
23 return {error};
24 }
25
26 reset = () => {
27 this.setState({error: null});
28 }
29
30 render() {
31 let {error} = this.state;
32 let {children, fallback: Fallback} = this.props;
33
34 if (error) {
35 return <Fallback reset={this.reset} error={error} />;
36 }
37
38 return children;
39 }
40}

This implementation is pretty simple, it uses the getDerivedStateFromError lifecycle method to catch any errors thrown from its children. If an error is caught, it will render the fallback component, passing in the error and a reset function.

This reset function can be used to reset the error state, allowing the user to retry whatever action caused the error in the first place.

Here's an example implementation:

1import {ErrorBoundary} from "./ErrorBoundary";
2
3function ErrorFallback({reset, error}) {
4 return (
5 <div>
6 <p>Oh no, an error has occurred</p>
7 <button onClick={reset}>Retry</button>
8 </div>
9 );
10}
11
12function MyComponent() {
13 return (
14 <ErrorBoundary fallback={ErrorFallback}>
15 <ComponentThatMayThrow />
16 </ErrorBoundary>
17 );
18}
1import {ErrorBoundary} from "./ErrorBoundary";
2
3function ErrorFallback({reset, error}) {
4 return (
5 <div>
6 <p>Oh no, an error has occurred</p>
7 <button onClick={reset}>Retry</button>
8 </div>
9 );
10}
11
12function MyComponent() {
13 return (
14 <ErrorBoundary fallback={ErrorFallback}>
15 <ComponentThatMayThrow />
16 </ErrorBoundary>
17 );
18}