概要
AngularでエラーハンドリングをまとめようとしたらTypeScriptのthisの扱いにハマった話です。
環境
- TypeScript 2.5.3
- Angular 4.4.6
どんな問題が起きていたか
APIコールのロジックでエラーが発生した時のハンドリングをまとめることができるよう、共通のErrorServiceを持っていました。
ただ時々ErrorServiceでCannot read property 'navigateByUrl' of undefined
というエラーが出てきていました。
共通のエラーハンドラー
以下のように共通のロジックを持っていました。
@Injectable() export class ErrorService { constructor( private router: Router, ) { } handleError(error: HttpErrorResponse): Observable<any> { if (error.status === 503) { this.router.navigateByUrl('/maintenance'); } if (error.status === 401) { Store.removeItem('auth'); this.router.navigateByUrl('/login'); } if (/^4[0-9]{2}$/.test('' + error.status) === false) { Raven.captureException(error); } return Observable.throw(error); } }
- 503エラーの時はメンテナンスページに飛ばす
- 401エラーの時は認証に失敗したので、セッション情報を消して強制的にログアウトさせる
- それ以外で4xxではないエラーであれば、Exceptionを検知ツールへ通知する
というロジックです。
APIコールでの利用
以下のようにObservableのcatch
でエラーを掴んだら、上で定義していたErrorServiceを呼ぶようにしていました。
@Injectable() export class StatusService { constructor( private http: HttpClient, private errorService: ErrorService ) {} fetch(): Observable<Status> { return this.http.get<Status>(environment.api + '/status') .catch(this.errorService.handleError); } }
ErrorServiceは問題なくDIできているのに、なぜかhandleError()
内のthis.router
がundefined
と怒られることがあったのです。
原因と解決方法
同じ問題の人がいて、その解決方法に則ると以下の通りでした。
.catch(this.errorService.handleError);
という呼び出しを
.catch(error => this.errorService.handleError(error));
という形にする必要がありました。何故でしょうか?
TypeScriptのthisの扱い
TypeScriptのgithub wikiには以下のように書いています。
When a function is invoked in JavaScript, you can follow these steps to determine what
this
will be (these rules are in priority order):
- If the function was the result of a call to
function#bind
,this
will be the argument given tobind
- If the function was invoked in the form
foo.func()
,this
will befoo
- If in strict mode,
this
will beundefined
- Otherwise,
this
will be the global object (window
in a browser)
ref: 'this' in TypeScript
訳すと以下です。
bind
を使ってたら、this
は引数を指すfoo.func()
という呼び出し方であれば、this
はfoo
を指す- strict modeであれば、
this
はundefinedになる - それ以外は
this
はグローバルオブジェクトになる
この流れでいくと、以下のコードのthisが何を指しているかすんなり理解できます。
class Foo { x = 3; print() { console.log('x is ' + this.x); } } var f = new Foo(); f.print(); // Prints 'x is 3' as expected // Use the class method in an object literal var z = { x: 10, p: f.print }; z.p(); // Prints 'x is 10' var p = z.p; p(); // Prints 'x is undefined'
今回の問題に照らし合わせる
.catch(this.errorService.handleError);
という渡し方は、関数を直接渡しています。上の例で言うと
var p = z.p;
と同じですね。ということは前述したthisの区別で言うと、それ以外はthis
はグローバルオブジェクトになるに当てはまります。
したがってhandleError()
内のthisはグローバルオブジェクトを指すため、それはrouter
なんてプロパティを持っておらずundefined
というエラーが発生したわけです。
一方解決方法の
.catch(error => this.errorService.handleError(error));
の方は、foo.func()
という呼び出し方であれば、this
はfoo
を指すに当てはまるため、handleError()
内のthisはthis.errorServiceを指します。なので問題なくrouter
というプロパティにアクセスできます。
まとめ
普通は
var p = z.p;
みたいに関数を別の変数に渡すことなんてしないので気にならないthis
の法則でしたが、Observableって直接関数渡せるから短縮して書いちゃおみたいな横着をするとこんな問題にぶつかる事があります。
きちんと理解して使いましょう。