ReactのError Boundaryでエラーを受け止める方法
React のコンポーネントが描画中にエラーを投げると、React は画面をすべて消します。エラーが起きうる部分を Error Boundary で包んでおけば、壊れた部分だけを「表示できませんでした」のような代わりの表示に差し替え、ほかの部分はそのまま残せます。
筆者は検証環境で、自分で書いた Error Boundary と react-error-boundary を、12通りの場面で動かしました。どこで投げたエラーなら受け止められるか、イベントハンドラーで起きたエラーをどう渡すか、受け止めたあとでどう表示し直すかを確かめています。
検証環境は Ubuntu 24.04、Node.js 22.22.0 です。使ったのは react / react-dom 19.3.1、react-error-boundary 6.1.6、@types/react 19.3.0、typescript 7.0.2、esbuild 0.28.2、jsdom 29.1.1 です。.tsx を tsc で型チェックしてから esbuild で変換し、jsdom の上で動かしました。React は開発用のビルドです。GIF だけは、Playwright 1.56.1 で Chromium 141 を動かして撮りました。
Error Boundary がないと、画面がすべて消える
まず、Error Boundary を置かずに試しました。見出しの Header と、描画中にエラーを投げる Profile を並べただけの画面です。
export function Header() {
return <h1>マイページ</h1>;
}
export function Profile() {
if (flags.profileBroken) {
throw new Error('プロフィールを表示できません');
}
return <p>プロフィール: ねこ</p>;
}
export function NoBoundary() {
return (
<>
<Header />
<Profile />
</>
);
}
コードは抜粋で、import と flags の宣言は省いています。flags.profileBroken は、検証のために外から切り替えられるようにした値です。true にして描画すると、次のようになりました。
--- 1. Error Boundary なし ---
[onUncaughtError] プロフィールを表示できません
画面: (空)
エラーを投げたのは Profile だけですが、何の問題もない Header も一緒に消え、画面は空になりました。Component のリファレンスにある、Error Boundary の説明にも、描画中にエラーが投げられると React は UI を画面から取り除く、と書かれています。
[onUncaughtError] の行は、createRoot の2つ目の引数に渡した onUncaughtError が呼ばれたことを示しています。createRoot のリファレンスの、引数の説明のとおり、Error Boundary に受け止められなかったエラーで呼ばれる関数です。
Error Boundary はクラスコンポーネントで書く
Error Boundary は、static getDerivedStateFromError を持つクラスコンポーネントです。子のどこかが描画中にエラーを投げると、React はこのメソッドを呼びます。このメソッドが返した state をもとに、次の描画で代わりの表示に切り替わります。エラーの記録のような副作用は、componentDidCatch に書きます(getDerivedStateFromError のリファレンス)。
関数コンポーネントで同じことをする方法は、まだありません(Component のリファレンスの、関数コンポーネントについての注記)。そのため、クラスで1つ作って使い回すか、後で触れる react-error-boundary を使います。
筆者が検証に使ったのは、次の ErrorBoundary です。
import { Component, type ErrorInfo, type ReactNode } from 'react';
type FallbackProps = { error: unknown; reset: () => void };
type Props = {
children: ReactNode;
fallback: (props: FallbackProps) => ReactNode;
onError?: (error: unknown, info: ErrorInfo) => void;
};
// error には null や文字列が入ることもあるので、エラーの有無は別に持つ
type State = { hasError: boolean; error: unknown };
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };
// 子のどこかで描画中にエラーが投げられたら、次の描画で fallback を出す
static getDerivedStateFromError(error: unknown): State {
return { hasError: true, error };
}
// エラーの記録など、副作用はこちらで行う
componentDidCatch(error: unknown, info: ErrorInfo) {
this.props.onError?.(error, info);
}
reset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
return this.props.fallback({ error: this.state.error, reset: this.reset });
}
return this.props.children;
}
}
error の型を unknown にしたのは、JavaScript では Error 以外の値も投げられるからです。getDerivedStateFromError のリファレンスにも、引数には文字列や null が来る場合があると書かれています。null が来ても区別できるよう、エラーがあるかどうかは hasError に分けて持ちました。
fallback には、エラーと、表示し直すための reset を受け取る関数を渡します。検証では、エラーの文言と「もう一度試す」ボタンを出す Fallback を渡しました。
function Fallback({ error, reset }: { error: unknown; reset: () => void }) {
return (
<div role="alert">
<p>表示できませんでした({error instanceof Error ? error.message : String(error)})</p>
<button id="retry" onClick={reset}>もう一度試す</button>
</div>
);
}
export function WithBoundary({ log }: { log: (s: string) => void }) {
return (
<>
<Header />
<ErrorBoundary
fallback={(p) => <Fallback {...p} />}
onError={(error, info) => {
log(`componentDidCatch: ${(error as Error).message}`);
log(`componentStack:${info.componentStack}`);
}}
>
<Profile />
</ErrorBoundary>
</>
);
}
このコードも抜粋で、import は省いています。Profile だけを包んで描画した結果は、次のとおりです。
--- 2. Error Boundary あり ---
[onCaughtError] プロフィールを表示できません
[componentDidCatch] プロフィールを表示できません
[componentStack]
at Profile
at ErrorBoundary
at WithBoundary
画面: <h1>マイページ</h1><div role="alert"><p>表示できませんでした(プロフィールを表示できません)</p><button id="retry">もう一度試す</button></div>
Header は残り、Profile の場所だけが代わりの表示に変わりました。componentDidCatch のリファレンスにあるとおり、componentDidCatch が受け取る info.componentStack には、エラーを投げた Profile から上へ、親のコンポーネントの名前が並びます(出力では、ファイルの場所を省いています)。createRoot に渡した onCaughtError も呼ばれました。こちらは、Error Boundary が受け止めたエラーで呼ばれる関数です。
包んだ部分だけが、代わりの表示に変わる
ブラウザでも、包んだ部分だけが差し替わることを確かめました。画面の上側にある「いいね」の枠だけを ErrorBoundary で包み、下側の「メモ」の枠は外に置いています。「いいね」の数を表示するコンポーネントは、数が3になると描画中にエラーを投げるようにしました。

3回目のクリックで「いいね」の枠だけが「表示できませんでした」に変わり、「メモ」の枠はそのまま残りました。「もう一度試す」を押すと、いいねは 0 から表示し直されます。代わりの表示に切り替わった時点で、中のコンポーネントはツリーから取り除かれています。state の保持とリセットのページの、state はツリーの位置に結び付く、という説明のとおり、React がコンポーネントを取り除くと、その state も消えます。そのため、reset で Error Boundary の state を戻すと中のコンポーネントは新しく作られ、それまでの state は残りません。
どこを包むかについて、Component のリファレンスの、Error Boundary の説明は、コンポーネントを1つずつ包む必要はないとしています。エラーの文言を出すのが自然な場所に置く、という考え方です。たとえばメッセージアプリなら、会話の一覧や、メッセージ1件ずつを包むのは理にかなっているが、アバター画像1つずつまで包む意味はない、という例が挙がっています。
受け止められるエラーと、受け止められないエラー
公式の説明では、Error Boundary が受け止めるのは、描画の途中で投げられたエラーです。Component のリファレンスの注記には、受け止めないものとして、イベントハンドラー、サーバーでの描画、Error Boundary 自身が投げたエラー、setTimeout のような非同期のコードが挙がっています。例外として、useTransition の startTransition に渡した関数の中で投げたエラーは受け止めます。
それぞれを ErrorBoundary で包んで試した結果が、次の表です。これ以降のコードも、import の行があるもの以外は import を省いた抜粋です。表の各ケースでは、ErrorBoundary と Fallback で包む部分も省いています。
| エラーを投げた場所 | Error Boundary | 画面 |
|---|---|---|
描画中(Profile) |
受け止めた | 代わりの表示 |
useEffect の中 |
受け止めた | 代わりの表示 |
onClick の中 |
受け止めなかった | そのまま |
setTimeout の中 |
受け止めなかった | そのまま |
onClick で捕まえて、state の更新関数の中で投げ直す |
受け止めた | 代わりの表示 |
startTransition に渡した async 関数の中 |
受け止めた | 代わりの表示 |
onClick で投げた場合の出力です。
--- 3. イベントハンドラーで投げる ---
画面: <h1>マイページ</h1><button id="btn">送る</button>
> #btn をクリック
[window error] クリックで失敗
画面: <h1>マイページ</h1><button id="btn">送る</button>
画面は変わらず、エラーは jsdom の window の error イベントに届きました。onCaughtError も onUncaughtError も呼ばれていません。setTimeout の場合も画面は変わらず、エラーは Node.js の uncaughtException として出ました。どちらも、上の注記で受け止めないものとして挙がっている場所です。
useEffect の中で投げたエラーは受け止められ、代わりの表示に変わりました。useEffect は注記の「受け止めないもの」に入っていませんが、エフェクトについての説明もありません。useEffect の中のエラーが受け止められたのは、検証で確かめた結果です。
イベントハンドラーや setTimeout のエラーは、Error Boundary で包んでいても代わりの表示になりません。ボタンを押しても何も起きないように見えるので、次の方法で Error Boundary に渡すか、ハンドラーの中で自分でエラーを表示します。
イベントハンドラーのエラーを Error Boundary に渡す
イベントハンドラーで起きたエラーを代わりの表示にしたいときは、描画の流れにエラーを持ち込みます。検証環境で次の3つの方法を試し、どれも受け止められました。
1つ目は、ハンドラーで捕まえたエラーを、state の更新関数の中で投げ直す方法です。useState のリファレンスの、前の state をもとに更新する説明のとおり、更新関数は次の描画の途中で呼ばれます。そのため、そこで投げたエラーは Error Boundary に届きます。
function RethrowInSetState() {
const [, setState] = useState(0);
return (
<button
id="btn"
onClick={() => {
try {
throw new Error('クリックで失敗');
} catch (e) {
setState(() => { throw e; });
}
}}
>
送る
</button>
);
}
--- 6. ハンドラーで捕まえて、state の更新関数の中で投げ直す ---
画面: <h1>マイページ</h1><button id="btn">送る</button>
> #btn をクリック
[onCaughtError] クリックで失敗
画面: <h1>マイページ</h1><div role="alert"><p>表示できませんでした(クリックで失敗)</p><button id="retry">もう一度試す</button></div>
2つ目は、処理を startTransition に渡す方法です。useTransition のリファレンスの、Error Boundary でエラーを表示する例のとおり、渡した関数が投げたエラーや、返した Promise の失敗は、Error Boundary に届きます。検証では、await のあとで投げたエラーも受け止められました。
function ThrowInTransition() {
const [isPending, startTransition] = useTransition();
return (
<button
id="btn"
disabled={isPending}
onClick={() => {
startTransition(async () => {
await new Promise((r) => setTimeout(r, 10));
throw new Error('送信に失敗');
});
}}
>
送る
</button>
);
}
--- 7. startTransition(async)の中で投げる ---
画面: <h1>マイページ</h1><button id="btn">送る</button>
> #btn をクリック
[onCaughtError] 送信に失敗
画面: <h1>マイページ</h1><div role="alert"><p>表示できませんでした(送信に失敗)</p><button id="retry">もう一度試す</button></div>
3つ目は、react-error-boundary の README の、イベントハンドラーと非同期のエラーの説明で勧められている、useErrorBoundary を使う方法です。返ってくる showBoundary にエラーを渡すと、いちばん近い Error Boundary が代わりの表示に切り替わります。この方法は、react-error-boundary の ErrorBoundary で包んだときにだけ使えます。
import { ErrorBoundary as LibBoundary, useErrorBoundary } from 'react-error-boundary';
function ShowBoundaryButton() {
const { showBoundary } = useErrorBoundary();
return (
<button
id="btn"
onClick={() => {
fetchLike().catch((e) => showBoundary(e));
}}
>
送る
</button>
);
}
export function LibShowBoundary() {
return (
<>
<Header />
<LibBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<Fallback error={error} reset={resetErrorBoundary} />
)}
>
<ShowBoundaryButton />
</LibBoundary>
</>
);
}
fetchLike は、10ミリ秒待ってから「通信に失敗」を投げる、検証用の関数です。
--- 8. react-error-boundary の showBoundary ---
画面: <h1>マイページ</h1><button id="btn">送る</button>
> #btn をクリック
[onCaughtError] 通信に失敗
画面: <h1>マイページ</h1><div role="alert"><p>表示できませんでした(通信に失敗)</p><button id="retry">もう一度試す</button></div>
fallback の中で投げたエラーは、外側に届く
Error Boundary は、自分自身が投げたエラーを受け止めません。代わりの表示を描く途中でエラーが出たらどうなるかを、Error Boundary を二重にして確かめました。
export function BrokenFallback() {
return (
<ErrorBoundary fallback={() => <p>外側: 何かがうまくいきませんでした</p>}>
<Header />
<ErrorBoundary
fallback={() => {
throw new Error('fallback も失敗');
}}
>
<Profile />
</ErrorBoundary>
</ErrorBoundary>
);
}
--- 9. fallback の中で投げる ---
[onCaughtError] fallback も失敗
画面: <p>外側: 何かがうまくいきませんでした</p>
内側の代わりの表示が投げたエラーは、外側の Error Boundary が受け止めました。外側の Error Boundary の中にあった Header も、外側の代わりの表示に置き換わっています。アプリ全体を包む Error Boundary を1つ置いておくと、内側で受け止めきれなかったエラーで画面が空になるのを防げます。
エラーのあとで表示し直す
代わりの表示を出したままでは、ユーザーは先に進めません。表示し直す方法を2つ試しました。
reset で state を戻す
Fallback の「もう一度試す」は、ErrorBoundary の reset を呼び、hasError を false に戻します。原因が残ったまま押すと、もう一度同じエラーになり、代わりの表示のままでした。原因を直してから押すと、Profile が表示されました。次の出力は、さきほどの「2. Error Boundary あり」の続きです。
(原因を直さずに「もう一度試す」)
> #retry をクリック
[onCaughtError] プロフィールを表示できません
[componentDidCatch] プロフィールを表示できません
(中略)
画面: <h1>マイページ</h1><div role="alert"><p>表示できませんでした(プロフィールを表示できません)</p><button id="retry">もう一度試す</button></div>
(原因を直してから「もう一度試す」)
> #retry をクリック
画面: <h1>マイページ</h1><p>プロフィール: ねこ</p>
key や resetKeys で作り直す
表示するユーザーが変わったときのように、props が変われば直る場合もあります。ところが、ErrorBoundary は一度エラーを受け止めると、props が変わっても hasError を持ち続けます。broken というユーザーでエラーにしてから、表示するユーザーを neko に変えて試しました。
export function KeyReset({ userId }: { userId: string }) {
return (
<>
<Header />
<ErrorBoundary key={userId} fallback={(p) => <Fallback {...p} />}>
<UserProfile userId={userId} />
</ErrorBoundary>
</>
);
}
function UserProfile({ userId }: { userId: string }) {
if (userId === 'broken') throw new Error(`${userId} は表示できません`);
return <p>ユーザー: {userId}</p>;
}
key を付けない版(NoKeyReset)では、neko に変えても代わりの表示のままでした。
--- 10b. key を付けない場合(broken → neko) ---
[onCaughtError] broken は表示できません
画面: <h1>マイページ</h1><div role="alert"><p>表示できませんでした(broken は表示できません)</p><button id="retry">もう一度試す</button></div>
> userId を neko に変える
画面: <h1>マイページ</h1><div role="alert"><p>表示できませんでした(broken は表示できません)</p><button id="retry">もう一度試す</button></div>
ErrorBoundary に key={userId} を付けると、neko に変えたときに表示されました。
--- 10. key で作り直す(broken → neko) ---
[onCaughtError] broken は表示できません
画面: <h1>マイページ</h1><div role="alert"><p>表示できませんでした(broken は表示できません)</p><button id="retry">もう一度試す</button></div>
> userId を neko に変える
画面: <h1>マイページ</h1><p>ユーザー: neko</p>
key が変わると、React はそのコンポーネントを作り直し、state も初めからにします(state の保持とリセットのページにある、key でリセットする方法)。Error Boundary の hasError も、ほかの state と同じように消えます。
react-error-boundary の ErrorBoundary では、README の、省略できる props の説明のとおり、resetKeys に値の配列を渡すと、その値が変わったときに表示し直します。resetKeys={[userId]} にした版でも、neko に変えると表示されました。
--- 11. react-error-boundary の resetKeys(broken → neko) ---
[onCaughtError] broken は表示できません
画面: <h1>マイページ</h1><div role="alert"><p>表示できませんでした(broken は表示できません)</p><button id="retry">もう一度試す</button></div>
> userId を neko に変える
画面: <h1>マイページ</h1><p>ユーザー: neko</p>
エラーを記録する場所
受け止めたエラーを記録するには、Error Boundary の componentDidCatch か、createRoot に渡す onCaughtError を使います。createRoot のリファレンスの、本番でのエラーの記録では、onCaughtError・onUncaughtError・onRecoverableError の3つを使って、エラーを報告する仕組みを作る例が紹介されています。何も渡さないと、React はすべてのエラーをコンソールに出します。
なお、componentDidCatch のリファレンスの注意事項には、開発用のビルドでは受け止めたエラーも window まで伝わり、本番用のビルドでは伝わらない、と書かれています。ただし検証環境(React 19.3.1 の開発用ビルド、jsdom、onCaughtError を渡した状態)では、Error Boundary が受け止めたエラーで window の error イベントは起きませんでした。ブラウザと本番用のビルドでの動きは確かめていません。
Error Boundary は、描画中・useEffect の中・startTransition の中のエラーを受け止めます。イベントハンドラーや setTimeout のエラーは受け止めないので、state の更新関数で投げ直すか、startTransition や showBoundary で渡します。エラーのあとで表示し直すには、reset で state を戻すか、key を変えて作り直します。





