Reactのerror boundaryでキャッチされないエラーをキャッチできるようにする
INDEX
Reactのerror boundary
子コンポーネントでエラーが発生した時にエラー画面をフォールバックUIとして表示してくれる機能です。
今回試していませんが、error boundaryをシンプルで再利用可能なラッパーを提供しているライブラリも存在するようです。
2022年3月の時点では、関数コンポーネントのerror boundaryはサポートされていないので、クラスコンポーネントで記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // Update state so the next render will show the fallback UI. return { hasError: true }; } componentDidCatch(error, errorInfo) { // You can also log the error to an error reporting service logErrorToMyService(error, errorInfo); } render() { if (this.state.hasError) { // You can render any custom fallback UI return <h1>Something went wrong.</h1>; } return this.props.children; } } |
基本的な使い方はerror boundaryを作成して、以下のようにエラーをキャッチしたいコンポーネントをラップする形です。
こうすることでErrorBoundaryコンポーネントでラップした子コンポーネントで発生したエラーをキャッチしてくれるようになります。
1 2 3 4 5 6 7 8 9 10 |
import React from "react"; import ErrorBoundary from "./src/components/ErrorBoundary"; export const App = () => { return ( <ErrorBoundary> <Component /> </ErrorBoundary> ) } |
ただし公式ドキュメントにも記載されている通り、全てのエラーをキャッチしてくれるわけではありません。
error boundaryがキャッチしないエラー
error boundary は以下のエラーをキャッチしません:
- イベントハンドラ(詳細)
- 非同期コード(例:setTimeout や requestAnimationFrame のコールバック)
- サーバサイドレンダリング
- (子コンポーネントではなく)error boundary 自身がスローしたエラー
非同期コードで発生したエラーをキャッチできないのは困るので、error boundaryでもエラーをキャッチできるようにした方法がGitHubのissueに載っています。
1 2 3 |
setState(() => { throw new Error('hi') }) |
これでerror boundaryでも非同期コードで発生したエラーを補足できるようになりました。めでたしめでたし。
と言いたいところですが、大抵のWebアプリケーションではREST APIやGraphQLなどのWeb APIを叩くような非同期コードを実行することが多いです。 毎回適切にエラー処理が出来ていればいいのですが、エラー処理のし忘れやエラーを握りつぶしてしまうこともあります。人間だもの。
できることなら復旧可能なエラー以外は直ちにアプリケーションをクラッシュさせてエラー画面を表示したい、つまりerror boundaryでエラーをキャッチしたいのが心情です。 そこで探してみると別の方法が同じissueと辿ったissueで紹介されていました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
static getDerivedStateFromError(error) { // Update state so the next render will show the fallback UI. return { hasError: true }; } + componentDidMount() { + window.addEventListener("unhandledrejection", this.onUnhandledRejection); + } + + componentWillUnmount() { + window.removeEventListener("unhandledrejection", this.onUnhandledRejection); + } + + onUnhandledRejection = (event: PromiseRejectionEvent) => { + event.promise.catch((error) => { + this.setState(TopLevelErrorBoundary.getDerivedStateFromError(error)); + }); + }; |
これで非同期コードで発生したエラーをerror boundaryでキャッチできるようになりました。
Window: unhandledrejection イベント
unhandledrejection イベントとは何ぞやということで調べました。ステータスがrejectedのPromiseが誰にもキャッチされなかった時に、JavaScriptエンジンがグローバルエラーを生成し、 そのイベント名が unhandledrejection のようです。
error boundaryでWindowオブジェクトの unhandlerejection イベントを購読することで非同期コードで発生したエラーに対応しているんですね。
では、そもそもなぜerror boundaryは非同期コードのエラーをキャッチできないのでしょうか。
その謎を解明するべく、我々はアマゾンの奥地へと向かった。
error boundary が非同期コードをキャッチできない理由
色々と調べると素晴らしい記事を見つけました。
ざっくり理解した要約:
- Promiseのexecutorでエラーが発生すると、暗黙的にエラーをPromise.rejectとして扱う(Promise.rejectでエラーをラップする)[1]
- error boundaryは同期コードのエラーはキャッチできるが、Promise.rejectでラップされたエラーはawait(Promise.catch)で待ち受けていないので、エラーとしてではなくステータスがrejectedのPromiseとして扱われてcatchブロックをすり抜ける [2]
- 結果、error boundaryで非同期コードをキャッチできない
unhandledrejectionに対応したerror boundary
最後に unhandledrejection に対応したerror boundaryを再掲します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import React from "react"; export class TopLevelErrorBoundary extends React.Component<{}, { hasError: boolean }> { constructor(props: {}) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(_error: any) { return { hasError: true }; } componentDidMount() { window.addEventListener("unhandledrejection", this.onUnhandledRejection); } componentWillUnmount() { window.removeEventListener("unhandledrejection", this.onUnhandledRejection); } onUnhandledRejection = (event: PromiseRejectionEvent) => { event.promise.catch((error) => { this.setState(TopLevelErrorBoundary.getDerivedStateFromError(error)); }); }; componentDidCatch(error: any, errorInfo: any) { console.log("Unexpected error occurred!", error, errorInfo); } render() { if (this.state.hasError) { return ( <YourErrorViewHere /> ); } return this.props.children; } } |
まとめ
ここでerror boundaryがキャッチしないエラーを再掲します。
error boundary は以下のエラーをキャッチしません:
- イベントハンドラ(詳細)
- 非同期コード(例:setTimeout や requestAnimationFrame のコールバック)
- サーバサイドレンダリング
- (子コンポーネントではなく)error boundary 自身がスローしたエラー
今回は上記の非同期コードをキャッチできるようにする方法について調査しました。
イベントハンドラで発生したエラーについてはあまり調査ができていません。
GitHubにずばりなタイトル Why are Error Boundaries not triggered for event handlers? のissueを見つけましたのでリンクを貼っておきます。 経緯について興味がある方は調べてみてください。
本記事の執筆で自分が非同期 async/await(Promise)について何も理解していないことを痛感しました。
私は雰囲気で非同期コードを書いていた。
- https://ja.reactjs.org/docs/error-boundaries.html
- https://github.com/facebook/react/issues/14981#issuecomment-468460187
- https://github.com/facebook/react/blob/bc9bb87c2b01bff8a15e02c8416addf6177e9055/packages/react-reconciler/src/ReactFiberWorkLoop.new.js#L1582
- https://github.com/facebook/react/issues/14981#issuecomment-743916884
- https://github.com/facebook/react/issues/11409
- https://eddiewould.com/2021/28/28/handling-rejected-promises-error-boundary-react/
- https://developer.mozilla.org/ja/docs/Web/API/Window/unhandledrejection_event
- https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise
- https://ja.javascript.info/promise-error-handling#ref-865
- https://azu.github.io/promises-book/
- https://zenn.dev/uhyo/articles/unhandled-rejection-understanding
- https://dev.to/artemmalko/error-boundaries-in-react-how-its-made-3lam

美髭公

最新記事 by 美髭公 (全て見る)
- Reactのレンダリング、差分検出処理の仕組みを学ぶ - 2022年7月11日
- プロダクトにReact Testing Library(RTL)を導入してみてハマったこと - 2022年5月13日
- Reactのerror boundaryでキャッチされないエラーをキャッチできるようにする - 2022年3月17日
- Google Workload identity federationでGitHub Actionsを設定してみた - 2022年3月17日
- Reactのprops drilling(バケツリレー)とhooksに我々はどう立ち向かっていけばよいのか - 2021年11月8日
関連記事
最新記事
FOLLOW US
最新の情報をお届けします
- facebookでフォロー
- Twitterでフォロー
- Feedlyでフォロー