Menu

Reactのエラーバウンダリ:レンダリングのエラーをフォールバックで受け止める

エラーバウンダリは、中のコンポーネントのレンダリング中に投げられたエラーを捕まえ、真っ白な画面の代わりにフォールバックを表示するコンポーネントです。何を捕まえるか、クラスでの書き方、リセットの方法、react-error-boundaryライブラリを学びます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

エラーバウンダリは、中のコンポーネントのレンダリング中に投げられたエラーを捕まえ、アプリ全体をクラッシュさせる代わりにフォールバックのUIを表示するReactのコンポーネントです。そのためのフックはないので、static getDerivedStateFromError を定義したクラスコンポーネントでなければなりません。1回書けば、ツリーのどの部分の周りにも置けます。

「Break the profile」を押すと、子がレンダリング中に例外を投げます。

プロフィールはフォールバックに置き換えられ、ページのほかの部分は動き続けます。コンソールにはエラーが2回表示されます。Reactは捕まえたすべてのエラーを console.error で記録し、componentDidCatch がもう一度記録するからです。「Try again」は原因(broken)とバウンダリのエラーをクリアするので、プロフィールがまたレンダリングされます。reset から this.props.onReset(); を削除すると、何も原因を直していないので、Try againはすぐにまたフォールバックに戻ってしまいます。

クラスの仕組み

2つのメソッドが、クラスをエラーバウンダリにします。

  • static getDerivedStateFromError(error) は、子が例外を投げた直後、レンダリング中に実行されます。バウンダリをフォールバックに切り替えるstateの更新を返します。純粋でなければならず、ログもリクエストも禁止です。
  • componentDidCatch(error, info) は、フォールバックが画面に出た後に実行されます。エラーをレポートのサービスに送るような副作用に使います。info.componentStack は、例外を投げたコンポーネントとその上のコンポーネントを列挙します。
class ErrorBoundary extends Component {
    state = { error: null };

    static getDerivedStateFromError(error) {
        return { error }; // switch to the fallback
    }

    componentDidCatch(error, info) {
        reportError(error, info.componentStack); // your logging service
    }

    render() {
        if (this.state.error) return this.props.fallback;
        return this.props.children;
    }
}

フックの同等品はないので、関数コンポーネントだけで書いたアプリでも、このクラスが1つは必要です。クラスの構文になじみがなければ、クラスコンポーネントのページで説明しています。

バウンダリが捕まえるものと捕まえないもの

バウンダリが捕まえるのは、Reactがその内側のコンポーネントを処理している間に起きたエラーです。

  • レンダリング(関数コンポーネントの本体、クラスの render)
  • 子のクラスコンポーネントのライフサイクルメソッドとコンストラクタ
  • エフェクト(useEffect、useLayoutEffect)とrefコールバック
  • useで読んだrejectされたPromise。React 19では、フォームアクションや useTransition の startTransition に渡した関数が投げたエラーも

次のものは捕まえません。

  • イベントハンドラのエラー
  • 自分で始めた非同期のコード:setTimeout、.then() のコールバック、イベントハンドラの中の await
  • バウンダリ自身の render のエラー(1つ上のバウンダリが捕まえます)
  • サーバーレンダリング中のエラー

イベントハンドラが除かれているのは、レンダリングを壊さないからです。画面は正しい状態のままなので、ハンドラの中の try/catch で十分です。それでもバウンダリを表示したいなら、エラーをstateに保存し、次のレンダリングで投げます。

1つ目のボタンではボタンが画面に残り、コンソールに捕まえられなかったエラーが表示されるだけです。2つ目はrejectを捕まえて保存し、バウンダリが引き継ぎます。

バウンダリを置く場所

アプリ全体を1つのバウンダリで包めば真っ白な画面は防げますが、どんな小さなエラーでもすべてが置き換えられてしまいます。サイドバーのウィジェット、チャート、各ルートのように、独立して失敗しうる部分の周りにバウンダリを置いてください。そうすれば失敗してもユーザーが失うのはページではなく1つのパネルで済みます。

フォールバックを表示するのはStocksのパネルだけです。2つのウィジェットを1つの <ErrorBoundary> に入れると、動いているWeatherのパネルも一緒に消えます。

バウンダリがまったくないと、レンダリングのエラーが捕まえられなかったとき、Reactはツリー全体を画面から取り除きます。間違ったデータを表示する壊れたページよりは空のページのほうが安全ですが、ユーザーはそれをクラッシュとして受け取ります。

react-error-boundaryライブラリ

react-error-boundary パッケージはクラスを包み、リセットの補助機能を加えてくれます。このエディタでは使えないので、静的なブロックで示します。

import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';

function Fallback({ error, resetErrorBoundary }) {
    return (
        <div role="alert">
            <p>Something went wrong: {error.message}</p>
            <button onClick={resetErrorBoundary}>Try again</button>
        </div>
    );
}

<ErrorBoundary FallbackComponent={Fallback} onReset={() => refetch()} resetKeys={[userId]}>
    <Profile userId={userId} />
</ErrorBoundary>;

// Inside a child: hand an async error to the boundary
const { showBoundary } = useErrorBoundary();
fetchData().catch(showBoundary);

resetKeys は、並べた値のどれかが変わるとバウンダリをリセットし、showBoundary は前の例の「保存して投げる」技を行います。

React 19でのエラーの報告

React 19はエラーを新しい方法で報告します。バウンダリが捕まえたエラーは console.error で1回記録され、捕まえられなかったエラーは window.reportError に送られます。自分のサービスに送るには、createRoot にオプションを渡します。各オプションは、対応するデフォルトのログを置き換えます。

import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'), {
    onCaughtError(error, errorInfo) {
        // an error boundary caught it
        logToService('caught', error, errorInfo.componentStack);
    },
    onUncaughtError(error, errorInfo) {
        // no boundary caught it, and React removed the tree
        logToService('uncaught', error, errorInfo.componentStack);
    },
    onRecoverableError(error, errorInfo) {
        // React recovered by itself, for example after a hydration mismatch
        logToService('recovered', error, errorInfo.componentStack);
    },
});
root.render(<App />);

onRecoverableError は、ハイドレーションの不一致や、Reactが再試行したら消えたレンダリングのエラーのように、Reactが手助けなしに直したエラーのためのものです。hydrateRoot も同じ3つのオプションを受け付けます。

よくある間違い

例外を投げるコンポーネントの中にバウンダリを置く。バウンダリが捕まえるのは子からのエラーだけです。Profile が自分のJSXの周りに <ErrorBoundary> を描画し、自分の本体で例外を投げると、エラーはそれを通り越して1つ上のバウンダリに行きます。コンポーネントはその親から包んでください。

getDerivedStateFromErrorで副作用を起こす。これはレンダリング中に実行され、複数回実行されることもあるので、stateを返すだけにしてください。ログは componentDidCatch か、ルートの onCaughtError に書きます。

何も直さないリセット。バウンダリのエラーをクリアすると、同じpropsで同じ子が描画されます。それがまた例外を投げれば、ユーザーはフォールバックに戻ってしまいます。最初の例が onReset でしているように、まずエラーの原因を変え(再取得する、不正な入力をクリアする、key を変える)、それからエラーをクリアしてください。

アプリ全体に1つのバウンダリだけ。真っ白なページは防げますが、小さな失敗がすべてページ全体のエラーになります。機能ごとにバウンダリを追加し、トップレベルのものは最後の砦として残してください。

自分にもエラーを隠してしまうフォールバック。ユーザーには親切なメッセージが正解ですが、エラーが自分のログに届くことは確かめてください。React 19は捕まえたエラーをデフォルトでコンソールに記録しますが、本番ではそのコンソールはユーザーのものなので、componentDidCatch か onCaughtError からサービスにエラーを送ってください。

エラーバウンダリは <Suspense> と自然に組み合わさります。Suspenseが「まだ準備ができていない」を、バウンダリが「失敗した」を扱います。use の例のようにバウンダリをSuspenseの外側に置けば、1つのコンポーネントで読み込み、成功、失敗のすべてをカバーできます。

よくある質問

Reactのエラーバウンダリとは何ですか?

中にあるどれかのコンポーネントのレンダリング中に投げられたJavaScriptのエラーを捕まえ、代わりにフォールバックのUIを描画するコンポーネントです。エラーバウンダリがないと、レンダリング中のエラーでアプリ全体が画面から消えます。

エラーバウンダリのためのフックはありますか?

いいえ。エラーバウンダリは static getDerivedStateFromError(と、必要なら componentDidCatch)を持つクラスコンポーネントでなければなりません。1回書けば、関数コンポーネントからほかのコンポーネントと同じように使えます。あるいは、クラスを包んでくれる react-error-boundary をインストールしてください。

エラーバウンダリはイベントハンドラのエラーを捕まえますか?

いいえ。イベントハンドラはレンダリングの外で実行されるので、Reactが回復するためにバウンダリは必要ありません。ハンドラで try/catch を使い、バウンダリを表示したいならエラーをstateに保存してレンダリング中に投げてください。

エラーバウンダリは非同期のエラーを捕まえますか?

自分で始めた setTimeout、Promiseのコールバック、fetch().then() のエラーは捕まえません。use で読んだrejectされたPromiseは捕まえ、React 19ではフォームアクションや useTransition の startTransition に渡した関数の中で投げられたエラーも捕まえます。

エラーバウンダリをリセットするには?

エラーの原因を直した後で、そのエラーのstateをクリアします。通常はフォールバックの中のTry againボタンから行います。バウンダリの key を変えてもリセットされます。Reactが新しいインスタンスをマウントするからです。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める