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を使うと考えあぐねることがない。