Some experiences in javascript exception handling
はじめに
アプリケーションの安定性を向上させるため、フロントエンドプロジェクトでスクリプト例外管理の取り組みを進めてきました。本番環境で報告される JS エラーについて全体的なトラブルシューティングを実施し、スクリプト例外の頻度を下げることで関連アラームの精度向上に努めました。この分野で最近参照した関連情報も踏まえ、定期的にまとめを行っていきたいと思います。ここでは、JS の例外処理に関するいくつかの経験を紹介します。
基本概念から始めましょう
例外とは
まずは公式の定義を確認してみましょう。
Error オブジェクトは、ランタイムエラーが発生した際にスローされます。Error オブジェクトは、ユーザー定義の例外のためのベースオブジェクトとしても使用できます。
説明は非常にシンプルです。まとめると、コードが実行中に問題に遭遇し、プログラムが正常に実行できなくなり、Error オブジェクトがスローされるということです。これは多くのプログラミング言語で使用される例外オブジェクトとは異なり、「エラー」と呼ぶ方がより適切です。実際その通りです。Error オブジェクトがスローされない限り、JS 内の他の通常のオブジェクトと何ら変わらず、例外もスローされません。同時に、Error オブジェクトはユーザー定義のエラーベースオブジェクトとしても使用できます。
上記の赤いメッセージには、例外メッセージとスタックトレースが含まれており、コード内の問題を特定する上で重要な役割を果たします。スタックトレースは、下部のファイル位置 21:15 から上部のファイル位置 25:7 までの呼び出し履歴を示しています。最初の 2 つのコンソールは例外発生時には実行されず、2 番目のスクリプトタグ内のコードは正常に実行されます。
結論:処理されない例外が発生すると、コールスタックを層ごとに遡ってスローされ(イベントバブルに似ています)、最終的に現在のタスクが終了します。現在のタスクが終了した後、JS スレッドはタスクキューから次のタスクを取得して実行を続けます。
例外の処理方法
例外は避けられないため、ソフトウェア開発において適切な例外処理は高品質なコードの不可欠な要素です。例外を適切に処理することで、プログラムの予期しない状況を効果的に管理できます。最も陥りやすい問題の 1 つは、例外処理をビジネスプロセスと混同することです。
Clean Code の推奨に従い、以下の原則に基づいて例外処理のコード品質を向上させることができます。
エラーコードではなく例外を使用する
例外はエラーコードよりも推奨されます。
この一文を理解するために、例を見てみましょう。以下の最初のコードでは、Laptop クラスの sendShutDown メソッド内で、getID の戻り値にある無効なデバイス ID を if 文でチェックしています。エラーチェックにより呼び出し側のコードが複雑になり、ビジネスロジックが読みづらくなります。また、このエラーチェックを省略するとコードに問題が生じる可能性があります。エラー処理を言語自体に任せることで、全体の流れがより洗練されます。2 番目のコードでは例外処理とビジネスロジックを分離しており、以下のようなメリットがあります。
1. ビジネスプロセスがより明確で読みやすくなります。例外とビジネスロジックを別々の問題として独立して扱えます。
2. 分離された 2 つのロジックがより焦点を絞り、コードが簡潔になります。
3. プログラムの例外処理の責任がプログラミング言語に委譲され、境界が明確化されます。
キャッチしたエラーを無視しない
例外をキャッチした後の処理を無視しないでください。
以前のコードレビューでは、catch ブロック内で何も処理を行わないケースや、ESLint の検査のために console.log(error) を書くだけのケースがよく見られました。これも実質的には何も対処していないのと同じです。これは例外発生時に何の対策も講じない危険な処理方法です。これらの例外は通常、考慮されていない予期しない状況によって引き起こされ、ビジネスロジック内で発見が難しい問題を特定する手がかりとなります。一度これらの例外をキャッチしてしまうと、トップレベルのエラーモニタリングでは問題を自動的に検出できなくなります。プログラムがクラッシュしなくても、ユーザーから報告がない限り、どの機能が正常に動作していないのかを把握できません。そのため、少なくともこれらの例外はログに記録する必要があります。
reject された Promise を無視しない
Promise の例外を無視しないでください。適切に処理されていると確信できない限り。
この点について教訓を得たことがあります。AEM アクセスプロジェクトで、かつて reject された Promise のレポートを無効化し、すべての Promise 例外のキャプチャを禁止していました。当時、オンラインアプリケーションで発生する Promise 例外のほとんどは、UMI リクエストのインターフェイスエラーと Antd のフォーム検証エラーで、実際のオンライン問題は引き起こしていませんでした。そのため、キャッチされていない Promise 例外は無害だと安易に考えていました。この考え方は危険です。詳しく追跡したところ、インターフェイスリクエストではデータベース側で既に例外がキャッチされ、message.error で処理されていることが判明しました。フォーム検証エラーの例外も、Antd が処理後に引き続きスローしているものです。これら 2 つは本当に無害でした。しかし、より深刻な未処理の Promise 例外(たとえばインターフェイスが成功を返しても約束されたデータ形式が間違っている場合)に直面した際、同時にレポートしなければ、多くのオンライン問題の現場を見失い、ユーザーフィードバックに頼って推測と再現を繰り返すしかなくなりました。
例外の階層
カスタム例外を使用して、例外の階層を明確にします。
ビジネスコード内の例外を管理するのは非常に効果的です。前述の章では JavaScript が提供する基本的な例外タイプを紹介しました。これらの例外タイプはビジネスとは関係がないため、コード内のエラーを管理するにはビジネスに関連する例外をモデル化し、セマンティックな意味を持たせて、ビジネスロジックで特定の状況が発生した際にトリガーする必要があります。そうしないと、呼び出し側が例外をキャッチしてもその処理方法が分かりません。
これには以下のようなメリットがあります。
1. error instanceof CustomBizError を使用することで例外を簡単に識別でき、判定ロジックが簡潔で読みやすくなり、キャッチした例外の処理とプログラムの回復が容易になります。
2. カスタムエラークラスを標準化することで、上位レベルの処理が容易になります。たとえば、前述のインターフェイス例外をグローバルなスクリプト例外としてレポートしない選択ができます。インターフェイス例外では通常、関連情報が既にレポートされているためです。
例外にコンテキストを提供する
例外のコンテキストを提供します。
例外が発生した際には、通常、例外メッセージ、スタックトレース情報、ファイル名が提供され、エラーの箇所を特定できます。しかし、これらの情報だけでは問題の特定が難しい場合もあります。そのため、例外情報を充実させて、より迅速に問題を特定できるようにすることが推奨されます。例外がキャッチされた箇所で意図を説明できます。同時に、これらの追加情報は開発者が問題を特定するためのものであり、ユーザーが感知する必要はなく、ユーザーインターフェースに反映されるべきでもありません。
前節のカスタムエラーと合わせて、これらのカスタムエラーにもより豊富なコンテキスト情報を提供する必要があります。
React での推奨事項
ローカル UI の JS エラーがアプリケーション全体のクラッシュや空白画面を引き起こさないようにするべきです。影響範囲を最小限に抑えるという点は、合意形成しやすい結論です。そのため、React 16 ではエラーバウンダリーの概念が導入されました。
React Error Boundaries の公式ドキュメント [2] では以下のように説明されています。
エラーバウンダリーは、サブコンポーネントツリーの任意の箇所で発生する JavaScript エラーをキャプチャし、エラーを出力し、クラッシュしたサブコンポーネントツリーの代わりにフォールバック UI を表示する React コンポーネントです。エラーバウンダリーは、サブコンポーネントツリー全体のレンダリング中、ライフサイクルメソッド内、およびコンストラクタ内で発生するエラーをキャッチできます。
ProComponents [3] の多くのコンポーネントもエラーバウンダリーを使用すべきです。たとえば ProTable など、例外発生時にローカル UI のみに影響を制限するために使用します。@ant-design/pro-utils のソースコードは公式サイトの処理と同じです。詳細については、非常に詳しい解説がある公式サイトを参照してください。
ここから得られる示唆は、コンポーネントライブラリや業務システム内のブロックレベルのもの(SPM モデルの c ビット)には、必ずコンポーネントレベルの例外処理を考慮する必要があるということです。
例外のグローバルレポート
基本的には、これが予測不可能な例外に対処する究極のソリューションです。エラーレポートを自動的に収集し、しきい値に達するとアラームを発します。理想的には、例外発生後に開発チームが即座に問題を発見して特定できるようになります。主に 2 つのグローバルイベントを使用します。
window.onerror イベント
JS の実行中に発生するほとんどの例外(構文エラーを含む)は、window 上の error イベントをトリガーして登録された関数を実行します。try-catch とは異なり、1 つの error イベントで同期例外と非同期タスク例外の両方を検知できます(Promise 例外を除く)。使用方法は以下の通りです。
アプリケーションの安定性を向上させるため、フロントエンドプロジェクトでスクリプト例外管理の取り組みを進めてきました。本番環境で報告される JS エラーについて全体的なトラブルシューティングを実施し、スクリプト例外の頻度を下げることで関連アラームの精度向上に努めました。この分野で最近参照した関連情報も踏まえ、定期的にまとめを行っていきたいと思います。ここでは、JS の例外処理に関するいくつかの経験を紹介します。
基本概念から始めましょう
例外とは
まずは公式の定義を確認してみましょう。
Error オブジェクトは、ランタイムエラーが発生した際にスローされます。Error オブジェクトは、ユーザー定義の例外のためのベースオブジェクトとしても使用できます。
説明は非常にシンプルです。まとめると、コードが実行中に問題に遭遇し、プログラムが正常に実行できなくなり、Error オブジェクトがスローされるということです。これは多くのプログラミング言語で使用される例外オブジェクトとは異なり、「エラー」と呼ぶ方がより適切です。実際その通りです。Error オブジェクトがスローされない限り、JS 内の他の通常のオブジェクトと何ら変わらず、例外もスローされません。同時に、Error オブジェクトはユーザー定義のエラーベースオブジェクトとしても使用できます。
上記の赤いメッセージには、例外メッセージとスタックトレースが含まれており、コード内の問題を特定する上で重要な役割を果たします。スタックトレースは、下部のファイル位置 21:15 から上部のファイル位置 25:7 までの呼び出し履歴を示しています。最初の 2 つのコンソールは例外発生時には実行されず、2 番目のスクリプトタグ内のコードは正常に実行されます。
結論:処理されない例外が発生すると、コールスタックを層ごとに遡ってスローされ(イベントバブルに似ています)、最終的に現在のタスクが終了します。現在のタスクが終了した後、JS スレッドはタスクキューから次のタスクを取得して実行を続けます。
例外の処理方法
例外は避けられないため、ソフトウェア開発において適切な例外処理は高品質なコードの不可欠な要素です。例外を適切に処理することで、プログラムの予期しない状況を効果的に管理できます。最も陥りやすい問題の 1 つは、例外処理をビジネスプロセスと混同することです。
Clean Code の推奨に従い、以下の原則に基づいて例外処理のコード品質を向上させることができます。
エラーコードではなく例外を使用する
例外はエラーコードよりも推奨されます。
この一文を理解するために、例を見てみましょう。以下の最初のコードでは、Laptop クラスの sendShutDown メソッド内で、getID の戻り値にある無効なデバイス ID を if 文でチェックしています。エラーチェックにより呼び出し側のコードが複雑になり、ビジネスロジックが読みづらくなります。また、このエラーチェックを省略するとコードに問題が生じる可能性があります。エラー処理を言語自体に任せることで、全体の流れがより洗練されます。2 番目のコードでは例外処理とビジネスロジックを分離しており、以下のようなメリットがあります。
1. ビジネスプロセスがより明確で読みやすくなります。例外とビジネスロジックを別々の問題として独立して扱えます。
2. 分離された 2 つのロジックがより焦点を絞り、コードが簡潔になります。
3. プログラムの例外処理の責任がプログラミング言語に委譲され、境界が明確化されます。
キャッチしたエラーを無視しない
例外をキャッチした後の処理を無視しないでください。
以前のコードレビューでは、catch ブロック内で何も処理を行わないケースや、ESLint の検査のために console.log(error) を書くだけのケースがよく見られました。これも実質的には何も対処していないのと同じです。これは例外発生時に何の対策も講じない危険な処理方法です。これらの例外は通常、考慮されていない予期しない状況によって引き起こされ、ビジネスロジック内で発見が難しい問題を特定する手がかりとなります。一度これらの例外をキャッチしてしまうと、トップレベルのエラーモニタリングでは問題を自動的に検出できなくなります。プログラムがクラッシュしなくても、ユーザーから報告がない限り、どの機能が正常に動作していないのかを把握できません。そのため、少なくともこれらの例外はログに記録する必要があります。
reject された Promise を無視しない
Promise の例外を無視しないでください。適切に処理されていると確信できない限り。
この点について教訓を得たことがあります。AEM アクセスプロジェクトで、かつて reject された Promise のレポートを無効化し、すべての Promise 例外のキャプチャを禁止していました。当時、オンラインアプリケーションで発生する Promise 例外のほとんどは、UMI リクエストのインターフェイスエラーと Antd のフォーム検証エラーで、実際のオンライン問題は引き起こしていませんでした。そのため、キャッチされていない Promise 例外は無害だと安易に考えていました。この考え方は危険です。詳しく追跡したところ、インターフェイスリクエストではデータベース側で既に例外がキャッチされ、message.error で処理されていることが判明しました。フォーム検証エラーの例外も、Antd が処理後に引き続きスローしているものです。これら 2 つは本当に無害でした。しかし、より深刻な未処理の Promise 例外(たとえばインターフェイスが成功を返しても約束されたデータ形式が間違っている場合)に直面した際、同時にレポートしなければ、多くのオンライン問題の現場を見失い、ユーザーフィードバックに頼って推測と再現を繰り返すしかなくなりました。
例外の階層
カスタム例外を使用して、例外の階層を明確にします。
ビジネスコード内の例外を管理するのは非常に効果的です。前述の章では JavaScript が提供する基本的な例外タイプを紹介しました。これらの例外タイプはビジネスとは関係がないため、コード内のエラーを管理するにはビジネスに関連する例外をモデル化し、セマンティックな意味を持たせて、ビジネスロジックで特定の状況が発生した際にトリガーする必要があります。そうしないと、呼び出し側が例外をキャッチしてもその処理方法が分かりません。
これには以下のようなメリットがあります。
1. error instanceof CustomBizError を使用することで例外を簡単に識別でき、判定ロジックが簡潔で読みやすくなり、キャッチした例外の処理とプログラムの回復が容易になります。
2. カスタムエラークラスを標準化することで、上位レベルの処理が容易になります。たとえば、前述のインターフェイス例外をグローバルなスクリプト例外としてレポートしない選択ができます。インターフェイス例外では通常、関連情報が既にレポートされているためです。
例外にコンテキストを提供する
例外のコンテキストを提供します。
例外が発生した際には、通常、例外メッセージ、スタックトレース情報、ファイル名が提供され、エラーの箇所を特定できます。しかし、これらの情報だけでは問題の特定が難しい場合もあります。そのため、例外情報を充実させて、より迅速に問題を特定できるようにすることが推奨されます。例外がキャッチされた箇所で意図を説明できます。同時に、これらの追加情報は開発者が問題を特定するためのものであり、ユーザーが感知する必要はなく、ユーザーインターフェースに反映されるべきでもありません。
前節のカスタムエラーと合わせて、これらのカスタムエラーにもより豊富なコンテキスト情報を提供する必要があります。
React での推奨事項
ローカル UI の JS エラーがアプリケーション全体のクラッシュや空白画面を引き起こさないようにするべきです。影響範囲を最小限に抑えるという点は、合意形成しやすい結論です。そのため、React 16 ではエラーバウンダリーの概念が導入されました。
React Error Boundaries の公式ドキュメント [2] では以下のように説明されています。
エラーバウンダリーは、サブコンポーネントツリーの任意の箇所で発生する JavaScript エラーをキャプチャし、エラーを出力し、クラッシュしたサブコンポーネントツリーの代わりにフォールバック UI を表示する React コンポーネントです。エラーバウンダリーは、サブコンポーネントツリー全体のレンダリング中、ライフサイクルメソッド内、およびコンストラクタ内で発生するエラーをキャッチできます。
ProComponents [3] の多くのコンポーネントもエラーバウンダリーを使用すべきです。たとえば ProTable など、例外発生時にローカル UI のみに影響を制限するために使用します。@ant-design/pro-utils のソースコードは公式サイトの処理と同じです。詳細については、非常に詳しい解説がある公式サイトを参照してください。
ここから得られる示唆は、コンポーネントライブラリや業務システム内のブロックレベルのもの(SPM モデルの c ビット)には、必ずコンポーネントレベルの例外処理を考慮する必要があるということです。
例外のグローバルレポート
基本的には、これが予測不可能な例外に対処する究極のソリューションです。エラーレポートを自動的に収集し、しきい値に達するとアラームを発します。理想的には、例外発生後に開発チームが即座に問題を発見して特定できるようになります。主に 2 つのグローバルイベントを使用します。
window.onerror イベント
JS の実行中に発生するほとんどの例外(構文エラーを含む)は、window 上の error イベントをトリガーして登録された関数を実行します。try-catch とは異なり、1 つの error イベントで同期例外と非同期タスク例外の両方を検知できます(Promise 例外を除く)。使用方法は以下の通りです。
Related Articles
-
A detailed explanation of Hadoop core architecture HDFS
Knowledge Base Team
-
What Does IOT Mean
Knowledge Base Team
-
6 Optional Technologies for Data Storage
Knowledge Base Team
-
What Is Blockchain Technology
Knowledge Base Team
Explore More Special Offers
-
Short Message Service(SMS) & Mail Service
50,000 email package starts as low as USD 1.99, 120 short messages start at only USD 1.00
