先日 ng-japan OnAir を視聴していたところ「エラーハンドリングには ErrorHandler を実装する事が多いと思いますが、Router にもハンドラを渡す事ができるんですよ」なんて会話がありました。
なぬ? えらーはんどらあ??
シラナカッタ....。
という事でエラーハンドリングについて調べた事をメモしておきます。
環境
- angular 9.1
- node 13.9
ランタイムのJSエラーをキャッチする
ErrorHandler を継承したクラスを実装します。
// src/app/custom-error-handler.ts
import { ErrorHandler } from '@angular/core';
export class CustomErrorHandler implements ErrorHandler {
handleError(e) {
console.warn('👹ErrorHandler👹', e);
}
}
クラスを AppModule の providers で提供します。
import { ErrorHandler } from '@angular/core';
import { CustomErrorHandler } from './custom-error-handler';
@NgModule({
declarations: [ AppComponent ],
imports: [ BrowserModule ],
providers: [
{
provide: ErrorHandler,
useClass: CustomErrorHandler, // これ
},
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
ボタンを設置して、わざとランタイムエラーを発生させてみましょう。
// app.component.html
<button (click)="test()">test</button>
// app.component.ts
export class AppComponent implements OnInit {
test() {
// TypeError: Cannot read property 'id' of undefined
return [].find(_ => false).id;
}
}
カスタムの ErrorHandler がランタイムの JS エラーをキャッチしている事が分かります。
500 ページに飛ばす
エラーをキャッチした後「予期しないエラーが発生しました」ページに飛ばしたい事もあるかもしれません。
その場合はこのように実装すれば良さそうです。
// src/app/custom-error-handler.ts
// コンストラクタにインジェクトするため Injectable を宣言
@Injectable()
export class CustomErrorHandler implements ErrorHandler {
constructor(
private router: Router,
private zone: NgZone,
) {}
handleError(e) {
console.warn('👹ErrorHandler👹', e);
this.zone.run(() => {
// URLに "/500" を表示せず画面遷移する
this.router.navigate(['/500'], { skipLocationChange: true });
});
}
}
HTTP エラーをキャッチする
HttpInterceptor を継承したクラスを実装します。
// src/app/custom-interceptor.ts
import { HttpInterceptor } from '@angular/common/http';
@Injectable()
export class CustomInterceptor implements HttpInterceptor {
intercept(
req: HttpRequest<any>,
next: HttpHandler,
): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
catchError((e) => {
console.warn('👹HttpInterceptor👹', e);
// 呼び出し元で catchError() できるように例外処理
return throwError(e);
}),
);
}
}
クラスを AppModule の providers で提供します。
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { CustomInterceptor } from './custom-interceptor.ts';
@NgModule({
declarations: [ AppComponent ],
imports: [ BrowserModule, HttpClientModule ],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: CustomInterceptor, // これ
multi: true,
}
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
ボタンを設置して、わざとエラーを発生させてみましょう。
// app.component.html
<button (click)="test2()">test2</button>
// app.component.ts
export class AppComponent implements OnInit {
test2() {
// 存在しない URL へのリクエスト
this.http.get(
'http://dummy.restapiexample.com/api/v1/dummy-request',
).pipe(
catchError(() => EMPTY)
).subscribe();
}
}
カスタムの Intercepter が HTTP エラーをキャッチしている事が分かります。
ルーターのエラーをキャッチする
Router の errorHandler を変更します。
// app.component.ts
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
constructor(
private router: Router,
) {}
ngOnInit() {
this.router.errorHandler = (e) => {
console.warn('👹router ErrorHandler👹', e);
this.router.navigate(['/403'], { skipLocationChange: true });
};
}
※ errorHandler をどこでセットするのが正しいのか、情報が見つからなかったためとりあえず AppComponent.ngOnInit で宣言しました。
アンカータグを設置して、わざと存在しないページへの遷移を発生させてみましょう。
// app.component.html
<a routerLink="/path/to/invalid">invalid</a>
router.errorHandler が存在しないページへの遷移をキャッチしている事が分かります。
なお、トップページにリダイレクトするようなフォールバックを指定している場合は、ルーターの errorHandler は実行されません。
// app-router.module.ts
const routes: Routes = [
{ ... },
{ // リダイレクトによりエラーが発生しない
path: '**',
pathMatch: 'full',
redirectTo: '',
}
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ],
})
export class AppRouterModule {}
おわり
Angular のエラーハンドリングを利用すれば簡単にエラーがキャッチできますね。サンプルのコードでは console.warn
しているだけですが、実際の運用ではログ収集サービスにエラー情報を投げるなど、いろいろ有効活用できそうです。