Error Boundaryの仕組み:レンダリングエラーをキャッチする
なぜError Boundaryはクラスコンポーネントのみなのか。
getDerivedStateFromErrorと
componentDidCatchが内部でどう動くのかを徹底解説します。
🔥 問題提起:レンダリングエラーがアプリを破壊する
コンポーネントのレンダリング中に予期しないエラーが発生すると、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ならクラスコンポーネントのライフサイクルで処理されます。
// 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の設計上の理由であり、技術的な制約と意図的な選択の両面があります。
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;
}
} 理由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つのライフサイクルメソッドは呼ばれるフェーズが異なります。これが重要です。
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を提供します。
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>
);
} 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; }) 相当 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へ(何でも可) 🧩 実装パターン:どこに配置すべきか
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のネスト組み合わせがベストプラクティス