ReactのError Boundary配置で障害範囲が読めない実装をレビューする観点
はじめに
ReactのError Boundaryは、画面の一部で例外が起きたときにアプリ全体のクラッシュを避けるために使える。
ただし、配置が雑だとレビューでかなり読みづらくなる。
典型的には次の2パターンがある。
- アプリ全体を1つのError Boundaryで包み、どこで壊れても全画面エラーにする
- 小さな部品ごとにError Boundaryを置き、どこまで復旧できるのか分からなくする
Error Boundaryは「置いてあるか」ではなく、どの障害をどの範囲に閉じ込めるかが重要である。
この記事では、配置と責務をレビューする観点を整理する。
まず止めたい実装
次のように、アプリ全体を1つのError Boundaryで包む構成はよく見かける。
export function App() {
return (
<ErrorBoundary fallback={<FullPageError />}>
<Header />
<Routes>
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</ErrorBoundary>
);
}この構成自体が常に悪いわけではない。
ただし、ダッシュボード内の小さなウィジェットの描画エラーで、画面全体が落ちるなら過剰である。
@Reviewer: Error Boundaryがアプリ全体にしかないため、局所的な表示エラーでも全画面障害になります。壊れてよい範囲と残すべきナビゲーション範囲を分けて配置を検討してください。Error Boundaryの責務は例外捕捉だけではない
レビューで見るべき責務は、単なるcatchではない。
- 障害範囲をどこに閉じるか
- fallbackでユーザーに何を見せるか
- 復旧操作をどこに置くか
- ログや監視に必要な情報を送るか
- 再試行でどの状態を初期化するか
これらが曖昧なError Boundaryは、例外を隠しているだけになる。
レビュー観点1:壊れてよい範囲で包んでいるか
Error Boundaryは、画面の責務単位に合わせて置くとレビューしやすい。
export function DashboardPage() {
return (
<main>
<ErrorBoundary fallback={<SalesSummaryError />}>
<SalesSummary />
</ErrorBoundary>
<ErrorBoundary fallback={<RecentOrdersError />}>
<RecentOrders />
</ErrorBoundary>
<ErrorBoundary fallback={<NotificationPanelError />}>
<NotificationPanel />
</ErrorBoundary>
</main>
);
}この形なら、売上サマリーが壊れても注文一覧や通知は残せる。
ただし、何でも細かく包めばよいわけでもない。
レビューでは次を確認する。
- その部品だけ落としても画面の意味が保てるか
- fallbackが隣接UIと矛盾しないか
- 親の状態を壊したまま子だけ復旧しようとしていないか
- ユーザー操作の継続に必要な導線が残るか
レビュー観点2:fallbackが業務上の状態と混ざっていないか
fallbackは「例外が起きた状態」を表す。
それを、データなし、権限なし、読み込み中と同じ表示にしてしまうと、運用上の判断が難しくなる。
<ErrorBoundary fallback={<EmptyState message="データがありません" />}>
<BillingHistory />
</ErrorBoundary>この表示では、ユーザーにも運用者にも「本当にデータがない」のか「画面が壊れた」のか分からない。
@Reviewer: Error Boundaryのfallbackが空データ表示と同じになっています。例外発生と業務上の0件は意味が違うため、ユーザー向け文言と監視ログを分けてください。レビュー観点3:再試行で何をリセットするか決まっているか
fallbackに「再試行」ボタンを置く場合、ただ再レンダリングするだけでは復旧しないことがある。
export function RetryableBoundary({ children }: { children: React.ReactNode }) {
const [retryKey, setRetryKey] = useState(0);
return (
<ErrorBoundary
key={retryKey}
fallback={
<section role="alert">
<p>表示できませんでした</p>
<button onClick={() => setRetryKey(key => key + 1)}>
再試行
</button>
</section>
}
>
{children}
</ErrorBoundary>
);
}このように、再試行で境界内の状態を作り直す方針が明示されていると読みやすい。
ただし、フォーム入力や未保存状態まで消してよいかは別途判断が必要である。
レビューでは、再試行が次を満たすか確認したい。
- 壊れた範囲だけを再初期化する
- 未保存入力を消すなら明示する
- API再取得とレンダリング再試行を混同しない
- 同じ例外を無限に出し続けない
レビュー観点4:ログ責務が境界にあるか
Error Boundaryは、例外をユーザーに見せるだけでなく、監視へ渡す入口にもなる。
function DashboardBoundary({ children }: { children: React.ReactNode }) {
return (
<ErrorBoundary
fallback={<DashboardError />}
onError={(error, info) => {
reportError(error, {
componentStack: info.componentStack,
area: "dashboard",
});
}}
>
{children}
</ErrorBoundary>
);
}レビューでは、ログに必要な情報が残るかを見る。
- どの画面領域で起きたか
- 操作中の対象IDがあるか
- ユーザーに見せたfallbackと紐づけられるか
- 個人情報を過剰に送っていないか
例外を握りつぶすだけの境界は、障害調査を難しくする。
レビュー観点5:Error Boundaryで捕まらないエラーを理解しているか
Error Boundaryは万能ではない。
イベントハンドラ内の例外や非同期処理の失敗は、通常のError Boundaryだけでは扱えない。
function DeleteButton({ id }: { id: string }) {
async function handleClick() {
await deleteItem(id);
throw new Error("unexpected failure");
}
return <button onClick={handleClick}>削除</button>;
}このような処理は、イベントハンドラ側で失敗状態を扱う必要がある。
レビューでは、Error Boundaryに任せるエラーと、フォームや操作単位で扱うエラーを分けて読む。
@Reviewer: この失敗はユーザー操作に紐づく非同期エラーなので、Error Boundaryではなく操作単位のエラー状態として扱うべきです。削除失敗時の復旧導線をボタン周辺に置いてください。レビューコメント例
@Reviewer: Error Boundaryの配置がページ全体に寄っているため、局所的なウィジェット障害でも全画面エラーになります。ナビゲーションや他の情報を残す必要がある画面なので、障害隔離の単位を見直してください。@Reviewer: fallbackが「データなし」と同じ表示になっています。例外発生、0件、権限なしはユーザーの次の行動が違うため、表示とログを分けてください。まとめ
ReactのError Boundaryは、例外を捕まえるためだけの部品ではない。
レビューでは、障害をどこに閉じ込め、どう復旧し、どう観測するかを見る必要がある。
確認したいのは次の線である。
- 壊れてよい範囲で包んでいるか
- fallbackが業務状態と混ざっていないか
- 再試行で何を初期化するか明確か
- ログや監視に必要な情報が残るか
- 非同期エラーまでBoundary任せにしていないか
Error Boundaryは置けば安全になるものではない。
配置と復旧責務が読めるときに、初めてレビューしやすい障害設計になる。