Reactのerror boundaryでキャッチされないエラーをキャッチできるようにする
INDEX
Reactのerror boundary
子コンポーネントでエラーが発生した時にエラー画面をフォールバックUIとして表示してくれる機能です。
今回試していませんが、error boundaryをシンプルで再利用可能なラッパーを提供しているライブラリも存在するようです。
2022年3月の時点では、関数コンポーネントのerror boundaryはサポートされていないので、クラスコンポーネントで記述します。
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コンポーネントでラップした子コンポーネントで発生したエラーをキャッチしてくれるようになります。
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に載っています。
setState(() => {
throw new Error('hi')
})
これでerror boundaryでも非同期コードで発生したエラーを補足できるようになりました。めでたしめでたし。
と言いたいところですが、大抵のWebアプリケーションではREST APIやGraphQLなどのWeb APIを叩くような非同期コードを実行することが多いです。 毎回適切にエラー処理が出来ていればいいのですが、エラー処理のし忘れやエラーを握りつぶしてしまうこともあります。人間だもの。
できることなら復旧可能なエラー以外は直ちにアプリケーションをクラッシュさせてエラー画面を表示したい、つまりerror boundaryでエラーをキャッチしたいのが心情です。 そこで探してみると別の方法が同じissueと辿ったissueで紹介されていました。
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を再掲します。
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
この記事を書いた人
- 2013年にアーティスに入社。システムエンジニアとしてアーティスCMSを使用したWebサイトや受託システムの構築・保守に携わる。環境構築が好き。
関連記事
最新記事
FOLLOW US
最新の情報をお届けします
- facebookでフォロー
- Twitterでフォロー
- Feedlyでフォロー