子コンポーネントのレンダリング中に例外が投げられると、React はそのツリーごとアンマウントしてしまい、アプリ全体が真っ白になってしまいます。ユーザーから見れば「何も表示されず操作もできない」状態で、原因も分かりません。これを防ぐのが Error Boundary(エラーバウンダリ)です。レンダリング中のエラーをキャッチし、壊れた部分だけを用意しておいたフォールバックUIに差し替えられます。この記事では、Error Boundary が今もクラスコンポーネントでしか作れない理由を押さえたうえで、基本の実装、再試行ボタン付きのフォールバックUI、どこに置くべきか、そして Error Boundary では拾えないエラーまで、初心者〜中級者向けに整理します。
目次
レンダリング中のエラーがアプリ全体を巻き込む理由
React はコンポーネントのツリーを描画していく途中で例外が投げられると、その例外を握りつぶさずに上へ伝播させます。どこにもキャッチする仕組みがないと、React 16 以降は「壊れたUIをそのまま表示するより、まるごと外したほうが安全だ」という方針で、ルートのツリー全体をアンマウントします。結果として画面が真っ白になります。
たとえば、次のようにデータが null の可能性を考慮せずプロパティへアクセスすると、レンダリング中に例外が発生します。
type Props = { user: { name: string } | null };
function BuggyProfile({ user }: Props) {
// user が null のときに例外が投げられる
return <p>こんにちは、{user.name} さん</p>;
}
この BuggyProfile がページのどこか一箇所で使われているだけでも、Error Boundary で囲っていなければページ全体が消えてしまいます。特定のウィジェットの不具合が、無関係な部分まで道連れにしてしまうわけです。Error Boundary は、この巻き添えを食い止めるための境界線(バウンダリ)を引く仕組みだと考えると分かりやすいです。
Error Boundary はクラスコンポーネントで作る
最初に押さえておきたいのは、Error Boundary は現状クラスコンポーネントでしか作れないという点です。関数コンポーネントとフックには、レンダリング中のエラーを拾うための仕組みが用意されていません。Error Boundary として振る舞うには、次の2つのライフサイクルメソッドのどちらか(実務では両方)を持つクラスコンポーネントである必要があります。
| メソッド | 役割 |
|---|---|
static getDerivedStateFromError(error) | エラー発生時に呼ばれ、返した値で state を更新する。ここでフォールバックUIを表示する状態へ切り替える。レンダリング中に呼ばれるので副作用(ログ送信など)は書かない。 |
componentDidCatch(error, info) | エラー発生時に呼ばれ、エラー内容や発生箇所(info.componentStack)を受け取る。ログの記録や監視サービスへの送信など、副作用はこちらに書く。 |
役割を分けて考えると、getDerivedStateFromError は「表示を切り替えるための状態更新」、componentDidCatch は「エラーを記録するための副作用」を担当します。次の節で、この2つを持つ最小の Error Boundary を実装します。
基本の Error Boundary を実装する
子要素(children)を受け取り、エラーが起きたらフォールバックを表示するクラスコンポーネントを書きます。TypeScript では Component<Props, State> のようにプロパティと状態の型を指定します。
import { Component, type ErrorInfo, type ReactNode } from "react";
type Props = {
children: ReactNode;
fallback: ReactNode; // エラー時に表示する内容
};
type State = {
hasError: boolean;
};
class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
// ① エラー発生時に state を更新してフォールバックへ切り替える
static getDerivedStateFromError(_error: Error): State {
return { hasError: true };
}
// ② エラー内容を記録する(副作用はここ)
componentDidCatch(error: Error, info: ErrorInfo) {
console.error("Error Boundary がエラーを捕捉:", error);
console.error("発生箇所:", info.componentStack);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
export default ErrorBoundary;
ポイントは render の分岐です。state.hasError が true のあいだは fallback を、そうでなければ本来の children を返します。子コンポーネントのレンダリングで例外が投げられると、まず getDerivedStateFromError が呼ばれて hasError が true になり、続く再レンダリングでフォールバックが表示される、という流れです。実際に使うときは、守りたいコンポーネントを次のように囲みます。
import ErrorBoundary from "./ErrorBoundary";
import BuggyProfile from "./BuggyProfile";
function App() {
return (
<ErrorBoundary fallback={<p>プロフィールを表示できませんでした</p>}>
<BuggyProfile user={null} />
</ErrorBoundary>
);
}
export default App;
これで BuggyProfile がレンダリング中に例外を投げても、ページ全体は生き残り、「プロフィールを表示できませんでした」という代わりのUIが表示されます。境界の外側にある他の要素は、いつも通り動き続けます。
フォールバックUIと再試行(reset)を用意する
ただエラー文言を出すだけでなく、ユーザーがやり直せる導線があると親切です。エラー状態を元に戻す(reset)ボタンを付けてみます。フォールバックの見た目を柔軟にするため、ここでは fallback を「エラーとリセット関数を受け取って要素を返す関数」として渡せるようにします。
import { Component, type ErrorInfo, type ReactNode } from "react";
type FallbackRender = (args: {
error: Error;
reset: () => void;
}) => ReactNode;
type Props = {
children: ReactNode;
fallback: FallbackRender;
};
type State = {
hasError: boolean;
error: Error | null;
};
class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error("捕捉したエラー:", error, info.componentStack);
}
// エラー状態を初期化して、もう一度描画を試みる
reset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError && this.state.error) {
return this.props.fallback({
error: this.state.error,
reset: this.reset,
});
}
return this.props.children;
}
}
export default ErrorBoundary;
reset は hasError を false に戻すだけの関数です。呼ばれると再レンダリングが走り、children がもう一度描画されます。使う側は次のように、エラーメッセージと「再試行」ボタンを組み立てられます。
import ErrorBoundary from "./ErrorBoundary";
import Widget from "./Widget";
function Dashboard() {
return (
<ErrorBoundary
fallback={({ error, reset }) => (
<div role="alert">
<p>ウィジェットの読み込みに失敗しました。</p>
<pre>{error.message}</pre>
<button onClick={reset}>再試行</button>
</div>
)}
>
<Widget />
</ErrorBoundary>
);
}
export default Dashboard;
注意したいのは、reset はあくまでエラー状態を解除して再描画を試みるだけだという点です。エラーの原因(たとえば壊れたデータ)がそのまま残っていれば、再描画してもまた同じ例外が投げられ、すぐフォールバックに戻ります。「再試行」で本当に回復させたいなら、リセットと同時に取得しなおすなど、原因側を解消する処理も併せて用意します。
どこを囲むか──境界の粒度を決める
Error Boundary はアプリ全体を1つで囲むこともできますが、それだけだと「どこか一箇所でも壊れたら全部フォールバック」になり、境界を設けた意味が薄れます。基本の考え方は、壊れても他に影響させたくない単位ごとに、部分的に囲むことです。
たとえばダッシュボードなら、サイドバー・メイン・各ウィジェットをそれぞれ個別の Error Boundary で囲みます。こうすると、あるウィジェットが落ちてもその枠だけがフォールバックに差し替わり、他のウィジェットやナビゲーションは動き続けます。ページ全体を包む大きな境界を1つ用意しつつ、リスクの高い部分にはより内側に細かい境界を追加する、という二段構えもよく使われます。
function Layout() {
return (
// 外側:ページ全体の最終防衛ライン
<ErrorBoundary fallback={() => <p>問題が発生しました</p>}>
<Header />
<main>
{/* 内側:ウィジェット単位で個別に囲む */}
<ErrorBoundary fallback={() => <p>売上を表示できません</p>}>
<SalesWidget />
</ErrorBoundary>
<ErrorBoundary fallback={() => <p>通知を表示できません</p>}>
<NotificationWidget />
</ErrorBoundary>
</main>
</ErrorBoundary>
);
}
境界は下(子孫)で発生したエラーだけを捕捉します。SalesWidget のエラーはそれを囲む内側の境界が拾い、外側までは届きません。逆に Header でエラーが起きた場合は、内側の境界は関係なく、外側の境界が受け止めます。どこで落ちても影響範囲を最小にできるよう、境界の粒度を設計するのがコツです。
Error Boundary が拾えないエラー
Error Boundary は万能ではありません。捕捉できるのは、あくまで子コンポーネントのレンダリング中・ライフサイクル中・コンストラクタで投げられたエラーです。次のようなケースは仕組み上キャッチできないため、別の方法で対処する必要があります。
イベントハンドラの中のエラー
onClick などのイベントハンドラは、レンダリングとは別のタイミングで実行されます。そのため中で例外が投げられても Error Boundary は反応しません。ボタンのクリック処理などでエラーを扱いたいときは、ハンドラの中で try...catch を使って自分で捕捉します。
function SaveButton() {
const handleClick = () => {
try {
// 保存処理など。ここで throw しても Error Boundary は拾えない
save();
} catch (e) {
// 自分で捕まえて処理する
console.error(e);
alert("保存に失敗しました");
}
};
return <button onClick={handleClick}>保存</button>;
}
非同期処理(setTimeout・fetch など)のエラー
setTimeout のコールバックや fetch の .then の中で投げられたエラーも、レンダリングの外で起きるため捕捉できません。データ取得の失敗をフォールバックUIにつなげたい場合は、エラーを状態として保持し、レンダリング側で「エラー状態なら例外を投げる」ようにすると Error Boundary に流し込めます。もしくは非同期の失敗はその場で状態にして表示を切り替える、という素直な設計にします。
サーバーサイドレンダリング(SSR)中のエラー
Error Boundary はクライアント側でツリーを描画する際に働く仕組みです。サーバー側でHTMLを生成する SSR のレンダリング中に投げられたエラーは、この境界では捕捉されません。SSR 環境では、フレームワーク側のエラーハンドリング(後述の Next.js の仕組みなど)に任せることになります。
Error Boundary 自身のエラー
境界が捕捉するのは、あくまで子孫で発生したエラーです。Error Boundary コンポーネント自身の render やフォールバックUIの中で例外が投げられた場合、その境界では拾えず、さらに外側の Error Boundary へ伝播します。フォールバックUIはできるだけ単純に保ち、そこで新たなエラーを起こさないようにしておくのが安全です。
実務ではライブラリやフレームワークの仕組みも使う
仕組みを理解するには自作が一番ですが、実務では毎回クラスを書かずに済ませることも多いです。よく使われるのが react-error-boundary ライブラリで、関数コンポーネントのように扱える <ErrorBoundary> と、リセットや再試行のためのフックがまとまっています。内部ではやはりクラスコンポーネントが使われていますが、利用側はその存在を意識せずに済みます。
また Next.js(App Router)では、ルートのセグメントごとに error.tsx を置くだけで、そのセグメント内のレンダリングエラーを自動でキャッチしフォールバックを表示できます。これは Next.js が内部で Error Boundary をセットアップしてくれる仕組みで、ファイルを用意するだけで境界が張られます。ただし本記事の主題はあくまで React 一般の Error Boundary を自作することなので、Next.js 固有の error.tsx の詳細は別の話題として扱います。まずはここで見たクラス実装が、こうしたライブラリやフレームワークの土台になっていることを押さえておくと理解が深まります。
まとめ
Error Boundary は、子コンポーネントのレンダリング中に投げられた例外をキャッチし、アプリ全体が真っ白になるのを防いでフォールバックUIに差し替える仕組みです。現状はクラスコンポーネントでしか作れず、static getDerivedStateFromError でフォールバック用の状態に切り替え、componentDidCatch でエラーの記録などの副作用を行います。壊れても他に波及させたくない単位で部分的に囲むのが基本で、state を初期化する reset を用意すれば再試行の導線も作れます。一方で、イベントハンドラ内・非同期処理・SSR・境界自身のエラーは捕捉できないため、それぞれ try...catch や状態管理で個別に対処します。実務では react-error-boundary や Next.js の error.tsx も使われますが、その土台にあるのは今回書いたクラス実装です。まずは自作して仕組みを掴んでおきましょう。