グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



Reactのerror boundaryでキャッチされないエラーをキャッチできるようにする

Reactのerror boundary

error boundary は自身の子コンポーネントツリーで発生した JavaScript エラーをキャッチし、エラーを記録し、クラッシュしたコンポーネントツリーの代わりにフォールバック用の UI を表示する React コンポーネントです。error boundary は配下のツリー全体のレンダー中、ライフサイクルメソッド内、およびコンストラクタ内で発生したエラーをキャッチします。Error Boundary―React

子コンポーネントでエラーが発生した時にエラー画面をフォールバック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―React

非同期コードで発生したエラーをキャッチできないのは困るので、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 が非同期コードをキャッチできない理由

色々と調べると素晴らしい記事を見つけました。

ざっくり理解した要約:

  1. Promiseのexecutorでエラーが発生すると、暗黙的にエラーをPromise.rejectとして扱う(Promise.rejectでエラーをラップする)[1
  2. error boundaryは同期コードのエラーはキャッチできるが、Promise.rejectでラップされたエラーはawait(Promise.catch)で待ち受けていないので、エラーとしてではなくステータスがrejectedのPromiseとして扱われてcatchブロックをすり抜ける [2
  3. 結果、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 自身がスローしたエラー
Error Boundary―React

今回は上記の非同期コードをキャッチできるようにする方法について調査しました。

イベントハンドラで発生したエラーについてはあまり調査ができていません。

GitHubにずばりなタイトル Why are Error Boundaries not triggered for event handlers? のissueを見つけましたのでリンクを貼っておきます。 経緯について興味がある方は調べてみてください。

本記事の執筆で自分が非同期 async/await(Promise)について何も理解していないことを痛感しました。

私は雰囲気で非同期コードを書いていた。

この記事を書いた人

美髭公
美髭公事業開発部 web application engineer
2013年にアーティスに入社。システムエンジニアとしてアーティスCMSを使用したWebサイトや受託システムの構築・保守に携わる。環境構築が好き。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします