React Error Boundaryとは何か?仕組みと実装方法を理解する

公開日:
目次

Reactでエラーが発生すると画面が真っ白になってしまうことがあります。Error Boundaryを使うと、エラーが発生してもアプリ全体がクラッシュするのを防ぎ、適切なフォールバックUIを表示できます。

初心者でも「なんとなく使う」ではなく、Error Boundaryが何をしているのかを正確に理解しておくと、いざ問題が起きたときに適切な判断ができるようになります。

Reactでエラーが発生すると何が起きるか

Reactのコンポーネントでエラーが発生すると、そのエラーはコンポーネントツリー全体に波及します。結果として、画面が真っ白になったり、何も表示されない状態になります。

function BuggyComponent() {
  // undefinedのプロパティにアクセスしようとしてエラー
  const user = undefined;
  return <p>{user.name}</p>;  // TypeError発生
}

function App() {
  return (
    <div>
      <h1>マイアプリ</h1>
      <BuggyComponent />  {/* ここでエラーが発生 */}
      <Footer />          {/* このコンポーネントも表示されない */}
    </div>
  );
}

上記のコードでは、BuggyComponentでエラーが発生すると、Appコンポーネント全体が消えてしまいます。ユーザーには何が起きたのか分からず、アプリが使えなくなってしまいます。

Error Boundaryとは

Error Boundaryは、子コンポーネントで発生したエラーをキャッチし、エラー発生時にフォールバックUIを表示するReactコンポーネントです。

JavaScriptのtry-catchに似ていますが、Error BoundaryはReactコンポーネントのレンダリング中に発生するエラーをキャッチするために特別に設計されています。

Error Boundaryを使うと、エラーが発生した部分だけをフォールバックUIに置き換え、アプリの他の部分は正常に動作し続けることができます。

基本的な実装方法

Error Boundaryはクラスコンポーネントでしか実装できません。getDerivedStateFromErrorまたはcomponentDidCatchのいずれかのライフサイクルメソッドを定義することで、コンポーネントがError Boundaryになります。

関数コンポーネントとHooksが主流となった現在でも、Error Boundaryについては、React本体に関数コンポーネント用のAPIが提供されていないため、クラスコンポーネントで実装する必要があります。これはReact 19でも変わりません。ただし、後述するreact-error-boundaryライブラリを使えば、クラスコンポーネントを直接書く必要はなくなります。

import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  // エラー発生時にstateを更新
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  // エラーログの記録などに使用
  componentDidCatch(error, errorInfo) {
    console.error('エラーが発生しました:', error);
    console.error('エラー情報:', errorInfo.componentStack);
  }

  render() {
    if (this.state.hasError) {
      // フォールバックUI
      return <h1>問題が発生しました。</h1>;
    }

    return this.props.children;
  }
}

getDerivedStateFromErrorとcomponentDidCatchの違い

これら2つのメソッドは異なる目的で使用します。

getDerivedStateFromError

  • レンダリング中に呼ばれる
  • フォールバックUIを表示するためにstateを更新する
  • 副作用(ログ送信など)は禁止

componentDidCatch

  • コミットフェーズで呼ばれる
  • エラーログの送信やエラー追跡サービスへの報告に使用
  • 副作用を含めることができる

基本的には、getDerivedStateFromErrorでフォールバックUIを表示し、componentDidCatchでエラーをログに記録するという使い分けになります。

使い方

Error Boundaryはエラーをキャッチしたいコンポーネントを囲んで使います。

function App() {
  return (
    <div>
      <h1>マイアプリ</h1>
      <ErrorBoundary>
        <BuggyComponent />
      </ErrorBoundary>
      <Footer />  {/* このコンポーネントは影響を受けない */}
    </div>
  );
}

BuggyComponentでエラーが発生しても、Footerは正常に表示されます。エラーが発生した部分だけが「問題が発生しました。」というメッセージに置き換わります。

Error Boundaryの配置戦略

Error Boundaryをどこに配置するかは、エラー発生時にどの範囲を影響させたいかによって決まります。

アプリ全体を囲む

最もシンプルな方法です。どこでエラーが発生しても、アプリ全体がフォールバックUIに置き換わります。

function App() {
  return (
    <ErrorBoundary>
      <Header />
      <Main />
      <Footer />
    </ErrorBoundary>
  );
}

機能ごとに囲む

特定の機能でエラーが発生しても、他の機能には影響を与えません。

function App() {
  return (
    <div>
      <Header />
      <ErrorBoundary>
        <UserProfile />  {/* ここでエラーが発生しても */}
      </ErrorBoundary>
      <ErrorBoundary>
        <PostList />     {/* こちらは正常に表示される */}
      </ErrorBoundary>
      <Footer />
    </div>
  );
}

ユーザーの利便性を考えると、エラーの影響範囲を最小限にするために、機能ごとにError Boundaryを配置することを推奨します。

Error Boundaryがキャッチできないエラー

Error Boundaryは万能ではありません。以下のエラーはキャッチできません。

  • イベントハンドラ内のエラー - onClickなどのイベント内で発生したエラー
  • 非同期コード - setTimeoutfetch、Promiseなどの非同期処理内のエラー
  • サーバーサイドレンダリング - SSR中のエラー
  • Error Boundary自身のエラー - Error Boundaryコンポーネント自体で発生したエラー

イベントハンドラ内のエラーは通常のtry-catchで処理します。

function Button() {
  const handleClick = () => {
    try {
      // エラーが発生する可能性のある処理
      doSomething();
    } catch (error) {
      console.error('ボタンクリックでエラー:', error);
      // エラー処理
    }
  };

  return <button onClick={handleClick}>クリック</button>;
}

カスタマイズしたフォールバックUI

プロダクションでは、より親切なフォールバックUIを表示することが重要です。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // エラー追跡サービスに送信
    logErrorToService(error, errorInfo);
  }

  handleRetry = () => {
    this.setState({ hasError: false, error: null });
  };

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-container">
          <h2>問題が発生しました</h2>
          <p>ご不便をおかけして申し訳ありません。</p>
          <button onClick={this.handleRetry}>
            再試行
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

「再試行」ボタンを用意することで、ユーザーが自分でリカバリーを試みることができます。

react-error-boundaryライブラリ

毎回Error Boundaryをクラスコンポーネントで実装するのは手間がかかります。react-error-boundaryというライブラリを使うと、より簡潔に書けます。

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

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div>
      <p>問題が発生しました</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>再試行</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <MyComponent />
    </ErrorBoundary>
  );
}

ライブラリを使うことで、フォールバックUIを関数コンポーネントとして定義でき、リセット機能も簡単に実装できます。

開発環境と本番環境の違い

開発環境では、Reactはエラーが発生するとエラーオーバーレイを表示します。これはデバッグを容易にするためですが、Error Boundaryのフォールバックを確認したい場合は一度オーバーレイを閉じる必要があります。

本番環境ではエラーオーバーレイは表示されず、Error Boundaryのフォールバックがそのまま表示されます。

SuspenseとError Boundaryの関係

React 19ではSuspenseuseフックを使った非同期データの取得が推奨されています。SuspenseとError Boundaryは異なる役割を持ちますが、組み合わせて使うことで、ローディング状態とエラー状態の両方を適切に処理できます。

  • Suspense: データ読み込み中のローディング状態を管理
  • Error Boundary: エラーが発生した場合のフォールバックUIを表示
<ErrorBoundary fallback={<p>エラーが発生しました</p>}>
  <Suspense fallback={<p>読み込み中...</p>}>
    <UserProfile />
  </Suspense>
</ErrorBoundary>

この構成では、UserProfileがデータを読み込んでいる間はSuspenseのフォールバック(「読み込み中...」)が表示され、エラーが発生した場合はError Boundaryのフォールバック(「エラーが発生しました」)が表示されます。

SuspenseはPromiseをキャッチして待機状態を管理しますが、エラーのキャッチはError Boundaryの役割です。そのため、非同期処理を使うコンポーネントには、SuspenseとError Boundaryの両方を配置することが推奨されます。

参考

Error BoundaryとSuspenseについてより詳しく知りたい場合は、以下のリソースを参照してください。