JavaScriptでこのようなコードを見ることがあります。
const response = await request('https://...').catch((err) => {
logger.fatal(err);
});
意図としてはこのようなものだと推測できます。
- 成功時はそのまま
responseを使いたい - でも異常時はそのことをロギングしたい
-
try catchを書くのは面倒くさい🤖
ですがこのコードは大きな問題を孕んでいます。
問題がない場合 : 成功時
成功時は問題がありません。request()で得られた結果はそのまま定数のresponseに代入されます。
大問題の場合 : 失敗時
request()が失敗したときはpromise.catch()が呼び出されるのはお分かりいただけると思いますし、このコードを書いた人もそれを意図しているということも読み取れますが、このコードは異常終了する可能性がかなり高いです。
なんでそんなに危険なんですか
危険な理由はpromise.catch()を行った後の戻り値にあります。ただロギングをしているpromise.catch()のコールバックの戻り値はvoidです。この戻り値はresponseに代入されるためresponseはvoid(undefined)となってしまいます。
もしもこの後に以下のようなコードがあった場合undefinedに対するプロパティアクセスとして落ちてしまうでしょう。
const response = await request('https://...').catch((err) => {
logger.fatal(err);
});
return response.body;
わかりにくいこと
正常時はpromise.catch()は実行されずにawaitが行われると考えている
正常時も異常時も以下の順に処理が行われることを理解する必要があるでしょう。
request()promise.catch()await
promise.catch()も必ず実行されるというのがキモです。正常時もこのメソッドは呼ばれており、その結果何もしていないということを知っておく必要があります。
promise.catch()の内部的な実装はこのようになっており、単にpromise.then()のaliasであると理解していただければわかりやすいでしょう。
(実際のPromiseはクラスではなく、関数を使った記法であると思いますが、便宜的にクラスでの記法を採用します)
class Promise {
then(onfulfilled, onrejected) {
// ...
}
catch(onrejected) {
return this.then(null, onrejected);
}
}
promise.then()はじつは引数が2個あり、第1引数は成功時、第2引数は失敗時の処理を意味しています。第1引数のみを書く場合がほとんどだと思いますが、第2引数に書けばその時点でのエラーハンドリングができます。
第2引数が書かれていないpromise.then()は、そのPromiseがrejectedになったときはエラーハンドリングを行わず次のPromiseにそのまま処理を委譲します。これが最後のpromise.catch()まで伝播した結果がPromiseのチェインで最後に見ることができるpromise.catch()です。
逆に、第1引数が書かれていないpromise.then()は成功時はハンドリングを行わず、異常時にエラーハンドリングを行うことがおわかりいただけるかと思います。
TypeScriptでは
TypeScriptはpromise.then(), promise.catch()ともに以下のようなシグネチャを持っています。
interface Promise<T> {
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
}
どちらも注目していただきたいのは戻り値で、戻り値のPromiseの中の値を示すgenericがUnion typesになっていることがわかります。上記JavaScriptの例をTypeScriptで実装すると戻り値responseの型はResponse | undefinedとなることがすぐにわかります。
(Responseはそのリクエストの戻り値だと思ってください)
どうすればいいですか
async/awaitとthen/catchを同時に使うとこの問題が起こりやすいでしょう。そのためどちらかに統一して書くようにします。オススメはasync/awaitです。then/catchは同じ問題がこのメソッドの呼び出し側で起こりえます。
async/await
try {
const response = await request('https://...');
return response.body;
}
catch (err) {
logger.fatal(err);
}
then/catch
return request('https://...')
.then((response) => {
return response.body;
})
.catch((err) => {
logger.fatal(err);
});
結論
TypeScriptを使うと考えあぐねることがない。