ErrorBoundary を置いてあると、なんとなく安心してしまいます。実際にどこまで捕まえてくれるのか、5通りの壊し方を試して確かめました。
結果は、5つのうち2つだけでした。しかも捕まえてくれなかった3つのほうが、実務では圧倒的によく起きます。

試したこと
ErrorBoundary で包んだコンポーネントを5パターン用意し、それぞれ違う場所で例外を投げます。同時に window.onerror と unhandledrejection も購読しておいて、エラーが実際にどこへ届いたかを記録しました。
class Boundary extends React.Component {
static getDerivedStateFromError() { return { failed: true }; }
componentDidCatch(err) { log.boundary.push(err.message); }
render() { return this.state.failed ? <div>fallback</div> : this.props.children; }
}
window.addEventListener('error', e => log.windowError.push(e.message));
window.addEventListener('unhandledrejection', e => log.rejection.push(e.reason.message));
| 壊し方 | ErrorBoundary | 実際に届いた先 |
|---|---|---|
| レンダリング中に throw | 捕捉した | ErrorBoundary |
| useEffect の中で throw | 捕捉した | ErrorBoundary |
| イベントハンドラの中で throw | 素通り | window.onerror |
| setTimeout の中で throw | 素通り | window.onerror |
| async の await 後に throw | 素通り | unhandledrejection |
なぜ半分しか捕まえないのか
ErrorBoundary が見ているのは「React がコンポーネントを描画している最中」だけだからです。レンダリングと副作用の実行は React の管理下なので捕まえられます。
一方、イベントハンドラ・タイマー・非同期処理は、React の呼び出しスタックから外れたところで実行されます。ブラウザがイベントループから直接呼ぶので、Reactは介在していません。捕まえようがない、というのが正確なところです。
そして実務で起きるエラーは、圧倒的に後者に寄っています。
- 「保存」ボタンを押したらAPIが500を返して、そのあとの処理で落ちる
await fetch()のレスポンスが想定と違う形で、プロパティアクセスで落ちる- ポーリング処理が失敗し続けている
これらは全部 ErrorBoundary を素通りします。しかも画面は壊れないので、見た目には何も起きていないように見えます。「ボタンを押しても何も起きない」という報告だけが上がってくる、というのがこの状態です。
Script error. で30分溶かさないために
今回の検証中に、実際に引っかかったことがあります。最初はイベントハンドラのエラーが window.onerror にこう届きました。
window.onerrorに届いた: ["Script error."]
メッセージも行番号もスタックもありません。「何かエラーが起きた」以上の情報がゼロの状態です。
原因は、Reactを別ドメイン(CDN)から読み込んでいたことでした。ブラウザは、別オリジンのスクリプトで起きたエラーの詳細を、セキュリティ上の理由で隠します。スクリプトタグに crossorigin を付けて、配信側が CORS ヘッダーを返していれば開示されます。
<!-- 付ける前 -->
<script src="https://cdn.example.com/react.js"></script>
→ window.onerrorに届いた: ["Script error."]
<!-- 付けた後 -->
<script crossorigin="anonymous" src="https://cdn.example.com/react.js"></script>
→ window.onerrorに届いた: ["Uncaught Error: handler"]
属性1つで情報量がまったく変わります。エラー監視を入れる前に、これを確認しておかないと、収集したエラーの大半が Script error. になります。CDNからライブラリを読んでいる構成では必ず起きるので、監視ツールを入れる日の作業に含めておきたいところです。

3層で受け止める
ErrorBoundary だけでは足りないので、受け止める場所を分けます。
1. 予期できるものは、その場で処理する
APIが400を返す、通信に失敗する、といったものは例外ではなく想定内の分岐です。try/catch でその場で受けて、ユーザーに何が起きたかを表示します。
try {
await save(form);
} catch (e) {
setError('保存できませんでした。時間をおいて再度お試しください。');
reportError(e, { where: 'save', formId }); // 監視にも送る
}
ここで大事なのは、catch して握りつぶさないことです。画面には優しいメッセージを出しつつ、監視には生のエラーを送ります。ユーザーには見せないが記録は残す、が原則です。
2. 画面が壊れる系は ErrorBoundary
レンダリング中の例外はここで受けます。ポイントは置く粒度です。
アプリ全体を1つで包むと、どこか1か所の不具合で画面全体が真っ白になります。「この部分が壊れても、他は使える」という単位で分けて置くほうが被害が小さくなります。サイドバーのウィジェットが壊れても、記事本文は読めるべきです。
フォールバックUIには復帰の手段を置きます。「エラーが発生しました」だけだと、ユーザーはリロードしか選べません。再試行ボタンや、前の画面に戻る導線があると違います。
3. 取りこぼしはグローバルで拾う
window.addEventListener('error', e => report(e.error));
window.addEventListener('unhandledrejection', e => report(e.reason));
unhandledrejection の購読を忘れないでください。今回の検証でも、非同期のエラーは error イベントには来ませんでした。await を使うコードが増えるほど、こちらの比率が上がります。
ここは最後の砦なので、直せるものを拾う場所ではなく、想定外を検知する場所と考えます。ここに大量に流れてくるなら、1番と2番の設計が足りていないということです。
ノイズを減らさないと監視は死ぬ
グローバルで拾い始めると、自分たちのバグではないエラーが大量に流れ込みます。
- ブラウザ拡張が注入したスクリプトのエラー
件数としては最多になりがち ResizeObserver loop limit exceeded
実害がないのに大量に出る有名なもの- ユーザーが通信を切った・タブを閉じたことによる fetch の中断
- クローラーやボットの挙動によるもの
これらを放置すると、本物のエラーがその中に埋もれます。アラートが鳴りっぱなしになれば、そのうち誰も見なくなります。
対策は、送る前にフィルタすることです。スタックに自分たちのドメインが含まれないものを落とす、既知の無害なメッセージを除外する、といった処理を入口に置きます。「全部集める」より「対応するものだけ集める」ほうが、監視としては機能します。
この考え方はログ設計の記事で書いたことと同じです。対応する気のないものを記録しても、判断の材料にはなりません。
エラーに文脈をつける
スタックトレースだけでは、再現の手がかりになりません。送るときに情報を足します。
- どの画面・どの操作か
URLと、直前のユーザー操作 - ユーザーID
特定のユーザーだけかを判断するため。氏名やメールは送らない - リリースバージョン
いつのデプロイから増えたかが分かる - ソースマップ
これがないと圧縮されたコードの行番号しか出ない
ソースマップは公開ディレクトリに置かず、監視ツール側にアップロードするのが定石です。ソースマップは元のコードを復元できるので、誰でも取得できる場所に置くとソースを配布しているのと同じになります。
まとめ
- ErrorBoundary が捕まえたのは5つ中2つ
レンダリング中と useEffect だけ - イベントハンドラ・setTimeout・async は素通りする
実務ではこちらのほうが多い - 素通りしたものは画面が壊れない
だから気づかれない unhandledrejectionを必ず購読するerrorイベントには来ない- CDNのスクリプトには
crossoriginを付ける
ないとScript error.しか取れない - ノイズを落としてから集める
全部集めると本物が埋もれる
「ErrorBoundary を置いたのでエラー対策済み」という認識がいちばん危ないと思います。置いてあるのに拾えていない範囲を、具体的に把握しておくことが対策の第一歩でした。
次に読む記事



