⚛️ React Boundary
発展 — 🛡️ Error Boundary

Error Boundaryの仕組み:レンダリングエラーをキャッチする

なぜError Boundaryはクラスコンポーネントのみなのか。 getDerivedStateFromErrorcomponentDidCatchが内部でどう動くのかを徹底解説します。

🔥 問題提起:レンダリングエラーがアプリを破壊する

コンポーネントのレンダリング中に予期しないエラーが発生すると、React 16以前はアプリ全体が真っ白になり、 何も表示されなくなっていました。

エラーがアプリ全体を壊す例

function UserCard({ user }) {
  // userがnullの場合、.nameでTypeErrorが発生
  return (
    <div>
      <img src={user.avatar} />     {/* TypeError: Cannot read properties of null */}
      <h2>{user.name}</h2>
    </div>
  );
}

// React 16以前:
// → アプリ全体がクラッシュ → 真っ白な画面
// → ユーザーはリロードするしかない

// React 16+(Error Boundary使用):
// → UserCardだけエラーUI → アプリの他の部分は動き続ける
  • コンポーネントのrender中の例外は通常のtry-catchでは捕まえられない
  • Reactの再帰的レンダリングはコールスタックをまたぐため
  • エラーが一箇所で起きてもReactツリー全体が破綻していた

🎯 結論から言う

Error BoundaryはSuspenseと同じ「throwをキャッチ」する仕組みです。

throwされた値がPromiseならSuspense、Errorならクラスコンポーネントのライフサイクルで処理されます。

throwされた値による処理の分岐
// Reactのレンダリング中にthrowが起きた場合の処理分岐
function handleThrow(fiber, thrownValue) {
  if (
    thrownValue !== null &&
    typeof thrownValue === 'object' &&
    typeof thrownValue.then === 'function'
  ) {
    // Promiseが投げられた → Suspenseバウンダリで処理
    handleSuspense(fiber, thrownValue);
  } else {
    // Errorが投げられた → Error Boundaryで処理
    handleError(fiber, thrownValue);
  }
}

🏛️ なぜError Boundaryはクラスコンポーネントのみなのか

これはReactの設計上の理由であり、技術的な制約と意図的な選択の両面があります。

Error Boundaryの最小実装(クラスコンポーネント)
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }
  
  // render phase中のエラーをキャッチ → stateを返してフォールバックUI
  static getDerivedStateFromError(error) {
    // このメソッドはpure(副作用を持ってはいけない)
    return { hasError: true, error };
  }
  
  // commit phase後に呼ばれる → ログ送信などの副作用
  componentDidCatch(error, info) {
    // info.componentStack はコンポーネントスタック
    logErrorToService(error, info.componentStack);
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div class="error-ui">
          <h2>予期しないエラーが発生しました</h2>
          <button onClick={() => this.setState({ hasError: false })}>
            再試行
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}
なぜ関数コンポーネントでError Boundaryが作れないか

理由1:getDerivedStateFromErrorのrender phase問題

getDerivedStateFromErrorはrender phaseで呼ばれます。 関数コンポーネントのrender(関数本体の実行)中にエラーが起きた場合、 そのコンポーネント自身がクラッシュしているため、stateの更新を「同コンポーネント内」で行うことが技術的に不可能です。 クラスコンポーネントのstaticメソッドは「その親クラスの別メソッドとして」呼ばれるため、これが可能です。

理由2:React Hooksの規則との衝突

Hooksはrender(関数本体)が完了することを前提に設計されています。 もしrender中にエラーが起きてthrowされると、Hooksの状態(フックキュー)が中途半端になります。 この状態でuseStateをエラーハンドリングに使うことは、Hook順序の不変性原則を破る可能性があります。

理由3:現在のReactチームの方針

React公式はreact-error-boundaryライブラリの使用を推奨しており、 関数コンポーネントでのError Boundary実装は現時点での優先事項に含まれていません。 React 19ではuseフックが導入されましたが、Error Boundaryの関数版APIはまだロードマップに入っていません。

🔍 getDerivedStateFromError vs componentDidCatch

2つのライフサイクルメソッドは呼ばれるフェーズが異なります。これが重要です。

2つのメソッドの違い

getDerivedStateFromError(render phase)

  • render phaseで呼ばれる(副作用なし)
  • 新しいstateを返すことでフォールバックUIに切り替える
  • 純粋関数(staticメソッド)である必要がある
  • Concurrent Modeでは複数回呼ばれる可能性がある
  • 「エラーが起きたことを記録してフォールバックUIを表示する」目的
static getDerivedStateFromError(error) {
  // ✅ stateを返すだけ(副作用禁止)
  return { hasError: true, errorMessage: error.message };
  
  // ❌ 副作用はNG
  // logError(error); // Concurrent Modeで複数回呼ばれうる
}

componentDidCatch(commit phase)

  • commit phaseで呼ばれる(副作用OK)
  • エラーログの送信、エラー追跡サービスへのレポートに使う
  • error(エラー本体)とinfo(コンポーネントスタック)を受け取る
  • stateを変更することも可能だが非推奨
componentDidCatch(error, info) {
  // ✅ 副作用OK(ログ送信など)
  Sentry.captureException(error, {
    contexts: {
      react: { componentStack: info.componentStack }
    }
  });
}

⚠️ キャッチできるエラーとできないエラー

Error Boundaryは「レンダリング中に同期的にthrowされたエラー」のみをキャッチします。 これ以外のエラーはキャッチできません。

✅ キャッチできるエラー

  • render(関数本体)中のエラー
  • クラスコンポーネントのrenderメソッド中
  • コンストラクタ中のエラー
  • ライフサイクルメソッド中のエラー
  • 子コンポーネントのrender中のエラー

❌ キャッチできないエラー

  • イベントハンドラ内のエラー
  • 非同期コード(setTimeoutなど)
  • useEffect / useLayoutEffect内のエラー
  • Server Componentsのエラー
  • Error Boundary自身のエラー
イベントハンドラのエラーを扱う方法
// ❌ Error Boundaryでキャッチできない
function BrokenButton() {
  const handleClick = () => {
    throw new Error('クリック中のエラー'); // ← イベントハンドラはキャッチ不可
  };
  return <button onClick={handleClick}>クリック</button>;
}

// ✅ イベントハンドラはtry-catchを自分で書く
function SafeButton() {
  const [error, setError] = useState(null);
  
  const handleClick = () => {
    try {
      throw new Error('クリック中のエラー');
    } catch (e) {
      setError(e);
    }
  };
  
  if (error) return <div>エラー: {error.message}</div>;
  return <button onClick={handleClick}>クリック</button>;
}

// ✅ useEffectのエラーを意図的にError Boundaryへ伝える方法
function ComponentWithEffect() {
  const [, setState] = useState(null);
  
  useEffect(() => {
    fetchData().catch(error => {
      // stateに「エラーをthrowする関数」をセットすることで
      // 次のrenderでError Boundaryに捕まえさせるハック
      setState(() => { throw error; });
    });
  }, []);
  
  return <div>コンテンツ</div>;
}

📦 react-error-boundaryライブラリ

Reactチームが推奨するreact-error-boundaryライブラリは、 クラスコンポーネントの実装の複雑さを隠蔽し、関数コンポーネントらしいAPIを提供します。

react-error-boundaryの基本的な使い方
import { ErrorBoundary } from 'react-error-boundary';

// フォールバックコンポーネント
function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>エラーが発生しました:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>再試行</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={(error, info) => {
        // Sentryなどへのログ送信
        logError(error, info.componentStack);
      }}
      onReset={() => {
        // リセット時の処理(データの再取得など)
      }}
    >
      <UserProfile userId={1} />
    </ErrorBoundary>
  );
}
useErrorBoundary Hook:エラーを手動でError Boundaryに渡す
import { useErrorBoundary } from 'react-error-boundary';

function DataFetcher() {
  const { showBoundary } = useErrorBoundary();
  
  useEffect(() => {
    fetchData()
      .then(setData)
      .catch(error => {
        // 非同期エラーをError Boundaryに手動で渡す
        showBoundary(error);
      });
  }, []);
  
  return <div>データ</div>;
}

// useErrorBoundaryはreactの内部のthrowハックをカプセル化している
// 先ほどの setState(() => { throw error; }) 相当
withErrorBoundary HOC:コンポーネントをラップする
import { withErrorBoundary } from 'react-error-boundary';

// Higher-Order Componentとして使う
const SafeUserProfile = withErrorBoundary(UserProfile, {
  FallbackComponent: ErrorFallback,
  onError: logError,
});

// 使い方は通常のコンポーネントと同じ
<SafeUserProfile userId={1} />

🔗 SuspenseとError Boundary:類似性と違い

SuspenseとError Boundaryは「throwをキャッチするバウンダリ」という共通の仕組みを持っています。 両者の違いはthrowされた値の型だけです。

実際によく使う組み合わせパターン
// Suspense + Error Boundaryの典型的な組み合わせ
function AsyncDataSection() {
  return (
    // Error Boundaryが外側(レンダリングエラーをキャッチ)
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      {/* Suspenseが内側(データ待機状態を管理) */}
      <Suspense fallback={<LoadingSpinner />}>
        <DataComponent />
        {/* DataComponentは:
            - データ未準備 → Promiseをthrow → Suspenseがキャッチ
            - データ破損   → Errorをthrow  → Error Boundaryがキャッチ */}
      </Suspense>
    </ErrorBoundary>
  );
}

// throwの型による分岐(Reactの内部処理概略)
// throw new Promise(...)    → Suspenseバウンダリへ
// throw new Error(...)      → Error Boundaryへ
// throw 'string'            → Error Boundaryへ(何でも可)
SuspenseとError Boundaryの比較表
Suspense
Error Boundary
throwする値
Promise(thenable)
Error(または任意)
実装方法
<Suspense> JSXタグ
クラスコンポーネント
回復
Promiseの解決後に自動
resetErrorBoundaryで手動
fallback
fallback prop
FallbackComponent
ネスト
可能
可能

🧩 実装パターン:どこに配置すべきか

Error Boundaryの配置戦略
function App() {
  return (
    // レベル1:アプリ全体のフォールバック
    <ErrorBoundary FallbackComponent={AppCrashFallback}>
      <Router>
        {/* レベル2:ページレベルのフォールバック */}
        <ErrorBoundary FallbackComponent={PageErrorFallback}>
          <Header />
          
          <main>
            {/* レベル3:ウィジェットレベルのフォールバック */}
            <ErrorBoundary FallbackComponent={WidgetErrorFallback}>
              <RecommendationWidget />
            </ErrorBoundary>
            
            <ErrorBoundary FallbackComponent={WidgetErrorFallback}>
              <UserActivityFeed />
            </ErrorBoundary>
          </main>
          
          <Footer />
        </ErrorBoundary>
      </Router>
    </ErrorBoundary>
  );
}

// ポイント:
// - ウィジェットが壊れてもページ全体は動き続ける
// - ページが壊れてもアプリ全体はクラッシュしない
// - 各レベルで異なるフォールバックUIを提供できる

⚠️ Error Boundary自身がクラッシュした場合

Error Boundaryコンポーネント自身がエラーを投げた場合、そのError Boundaryはキャッチできず、 さらに上位のError Boundaryに伝播します。このためError Boundaryをネストする設計が重要です。 最上位のError Boundaryまで捕まえられなければ、Reactはアンマウント処理を行います。

📌 まとめ

  • ✓ Error BoundaryはSuspenseと同じ「throwをキャッチ」する仕組みで動作する
  • ✓ throwされた値がPromiseならSuspense、Errorなら最も近いError Boundaryがキャッチする
  • ✓ クラスコンポーネントのみの理由:render phase中のstateへの安全なアクセス方法がHooksには存在しない
  • ✓ getDerivedStateFromErrorはrender phaseで純粋なstate更新のみ行う
  • ✓ componentDidCatchはcommit phaseで副作用(ログ送信)を行う
  • ✓ イベントハンドラ・useEffect・非同期コードのエラーはキャッチできない
  • ✓ react-error-boundaryライブラリでクラスコンポーネントの複雑さを隠蔽できる
  • ✓ Suspense + Error Boundaryのネスト組み合わせがベストプラクティス

関連記事