はじめに
こんにちは、テックリードのかどたみです。 私自身はじめてのフロントエンドネタです。AngularのErrorHandlerを利用したエラーハンドリングにおいてエラーが思うようにキャッチできず困ったことがあったので、その事象と解決方法を簡単に紹介します。
ErrorHandler
現状のプロジェクトでは予期せぬランタイムエラーをモニタリングサービスに通知したり、API実行時の特定のエラーでの共通処理などを独自のErrorHandler(以降、CustomErrorHandler)で処理しています。Angularの独自エラーハンドリングはとてもシンプルでErrorHandlerを継承したクラスを作成し、Moduleに読み込ませるだけです。
class CustomErrorHandler implements ErrorHandler { handleError(error: unknown): void { // ここでモニタリングサービスの通知や、エラー型ごとの独自処理を実装 } } @NgModule({ providers: [{provide: ErrorHandler, useClass: CustomErrorHandler}] }) class AppModule {}
何が問題だったか
一言で言うと、async関数の中で発生したエラーにおいて、CustomErrorHandlerに渡ってくるエラーオブジェクトが予期しない形のものに変換されてしまう、ということです。 実際に渡ってくるエラーを用いた処理を見てみましょう。ここではAPI呼び出しでエラーになることを想定します。まずCustomErrorHandlerの中身は以下です。
import { HttpErrorResponse } from '@angular/common/http'; class CustomErrorHandler implements ErrorHandler { handleError(error: unknown): void { consol.log(error); if (error instanceof HttpErrorResponse) { console.log('API呼び出しでエラーが発生しました。') } } }
ではまず、アプリケーションの同期的な関数の中でエラーを発生させてみましょう。
process(): void { throw new HttpErrorResponse({}); } process();
この処理におけるコンソールの出力は以下です。 これは直感的ですね。
▶ HttpErrorResponse {headers: HttpHeaders, status: 0, statusText: 'Unknown Error', url: null, ok: false, …} API呼び出しでエラーが発生しました。
続いてasync関数の中で以下のエラーを発生させてみましょう。
async asyncProcess(): Promise<void> { throw new HttpErrorResponse({}); } await asyncProcess();
コンソールには何が出力されるでしょうか。。。?
正解は以下のみです。
Error: Uncaught (in promise): HttpErrorResponse: {"headers":{"normalizedNames":{},"lazyUpdate":null,"headers":{}},"status":0,"statusText":"Unknown Error","url":null,"ok":false,"name":"HttpErrorResponse","message":"Http failure response for (unknown url): undefined undefined","error":null}
HttpErrorResponseをthrowしているはずなのに、「API呼び出しでエラーが発生しました。」は出力されません。 しかもオブジェクトとして出力されているのではなくテキストとして出力されています。。。 これでは思うようにエラーハンドリングができないですね。
もう少し詳しく見てみましょう。
console.table(error)
で中身を見てみると、以下のような出力になりました。
何やらいっぱい出てきますが、rejectionというプロパティにthrowしたHttpErrorResponseが存在することがわかりますね。ではこいつは誰が生成しているのかというと、その他のプロパティの型を見ればわかりますね!そう、zone.jsです!
ということで、zone.jsについて調べていたら解決法が見つかったので紹介します。
解決法
至って簡単です。
zone.jsに関する設定の一つである DISABLE_WRAPPING_UNCAUGHT_PROMISE_REJECTION
をtrueにすれば解決しました。
こちらを参考に以下のファイルを準備します。
(window as any).__zone_symbol__DISABLE_WRAPPING_UNCAUGHT_PROMISE_REJECTION = true;
それをpolyfills.tsでzone.jsより前にインポートしてあげます。
import './zone-flags'; import 'zone.js';
たったこれだけでCustomErrorHandlerで素のエラーオブジェクトが渡ってくるようになります。
▶ HttpErrorResponse {headers: HttpHeaders, status: 0, statusText: 'Unknown Error', url: null, ok: false, …} API呼び出しでエラーが発生しました。
めでたしめでたし。
このフラグによる処理の分岐がわかるコード上の処理はこちらなので、興味がある人は追ってみてください。
残るもやもや
さて、問題の解決はでき、予期した通りの動作をさせることには成功しましたが、私の気持ちは完全には晴れていません。なぜなら、なぜこの設定をしないとエラーオブジェクトが単純に取得できないのかわからないからです。 このオブジェクトをラップする設定がデフォルトである以上、何らかの恩恵を受けられる可能性が高いと考えています。しかし、現状それが全くわかっておらず喉に刺さった小骨のように心にもやもやを残しています。 ご存じの方がいらっしゃいましたらご教授いただけると幸いでございます。
さいごに
いかがでしたでしょうか?今回はAngularでのエラーハンドリングの落とし穴について紹介してみました。日本語の情報も出てこないので周知の事実でわざわざ記事にする人が居なかったのかもしれないですが、私はハマってしまったので同じような誰かのためになれば幸いです。
エモーションテックでは顧客体験、従業員体験の改善をサポートし、世の中の体験を変えるプロダクトを開発しています。プロダクトに興味のある方、Angularを使った開発に興味のある方、ぜひ採用ページからご応募をお願いいたします。