はじめに
Typescript を await/catch パターンで書いているとき、こんな感じでアーリーリターンをしたいことはないでしょうか?
一見するとこれでアーリーリターンができるように見えますが return で実行は止まりません。
const result: string | void = await asyncFunc()
.catch((e) => {
console.log('catch の中だよ')
console.log(e) // e: "something went wrong!"
console.log('return の前だよ')
return // ここでアーリーリターンしたい
console.log('return の後だよ')
})
console.log('catch の外だよ')
console.log(result)
実行結果
catch の中だよ
something went wrong!
return の前だよ
catch の外だよ
undefined
なぜなら .catch の中にある return は変数 result に値を返すからです。
変数 result は、上の例では何も代入されないので undefined になりましたが、例えば return を return 'foobar' に変えれば、'foobar' になります。
このようなとき、どうしたらアーリーリターンができるのかを調べました。
環境
% tsc --version
Version 4.4.3
元にするコード
まず、アーリーリターンがされないコードを示します。
これに手を加えて、関数 sub() の中でアーリーリターンができるようにします。
async function asyncFuncToReturnString(occurErr: boolean): Promise<string> {
return new Promise(((resolve, reject) => {
if (occurErr) {
reject('something went wrong!')
} else {
resolve('success!')
}
}))
}
async function asyncFuncToReturnVoid(occurErr: boolean): Promise<void> {
return new Promise(((resolve, reject) => {
if (occurErr) {
reject('something went wrong!')
} else {
resolve()
}
}))
}
async function sub(): Promise<void> {
const result = await asyncFuncToReturnString(true)
.catch((e) => {
console.log('catch の中だよ')
console.log(e)
console.log('return の前だよ')
return // ここでアーリーリターンしたい
console.log('return の後だよ')
})
console.log('catch の外だよ')
console.log(result)
return
}
function main() {
sub().then(() => {
console.log('finish')
}).catch((e) => {
console.log('catch:', e)
})
}
main()
実行結果
catch の中だよ
something went wrong!
return の前だよ
catch の外だよ
undefined
finish
アーリーリターンの実現方法
try-catch を使用する
いきなり await/catch パターンではないのですが、これが一番オーソドックスな方法だと思うので紹介します。
await した関数を try 節で囲んで catch 節の中で return します。
このように一般的な try-catch の書き方をすればアーリーリターンができます。
async function sub(): Promise<void> {
try {
const result = await asyncFuncToReturnString(true)
console.log(result)
} catch (e) {
console.log('catch の中だよ')
console.log(e)
console.log('return の前だよ')
return // ここでアーリーリターンされる
console.log('return の後だよ')
}
console.log('catch の外だよ')
return
}
実行結果
catch の中だよ
something went wrong!
return の前だよ
finish
エラーの有無で判断する
エラーを保持する変数 err を用意して .catch の中でキャッチしたエラーを代入します。
エラーが発生しなかった場合 .catch の中は実行されないので、変数 err は undefined になります。
なので err の値が undefined でないか否かを判別することでアーリーリターンが可能です。
if の比較は (err != null) で行ってももちろん OK です。
async function sub(): Promise<void> {
let err
const result = await asyncFuncToReturnString(true)
.catch((e) => {
console.log('catch の中だよ')
err = e
})
console.log('catch の外だよ')
if (err !== undefined) {
console.log(err)
console.log('return の前だよ')
return // ここでアーリーリターンされる
console.log('return の後だよ')
}
console.log(result)
return
}
実行結果
catch の中だよ
catch の外だよ
something went wrong!
return の前だよ
finish
await した関数の戻り値で判断する
関数が戻り値を返す場合
.catch の中に return を書かなければ (もしくは return のみ、または return undefined を書けば) エラーが発生した場合に変数 result が undefined になります。
なので result の値が undefined か否かを判別することでアーリーリターンが可能です。
if の比較は (result == null) で行ってももちろん OK です。
async function sub(): Promise<void> {
const result = await asyncFuncToReturnString(true)
.catch((e) => {
console.log('catch の中だよ')
console.log(e)
})
console.log('catch の外だよ')
if (result === undefined) {
console.log('return の前だよ')
return // ここでアーリーリターンされる
console.log('return の後だよ')
}
console.log(result)
return
}
実行結果
catch の中だよ
something went wrong!
catch の外だよ
return の前だよ
finish
関数が戻り値を返さない場合
.catch の中でキャッチしたエラーを return すれば、エラーが発生した場合に変数 result にエラーが代入されます。
なので result の値が undefined でないか否かを判別することでアーリーリターンが可能です。
if の比較は (result != null) で行ってももちろん OK です。
async function sub(): Promise<void> {
const result = await asyncFuncToReturnVoid(true)
.catch((e) => {
console.log('catch の中だよ')
return e
})
console.log('catch の外だよ')
if (result !== undefined) {
console.log(result)
console.log('return の前だよ')
return // ここでアーリーリターンされる
console.log('return の後だよ')
}
console.log(result)
return
}
実行結果
catch の中だよ
catch の外だよ
something went wrong!
return の前だよ
finish
アーリーリターンをせずエラーを返す場合
エラーが発生したときアーリーリターンをしない場合もあると思います。
そのような場合は、以下のようにすることで呼び出し元にエラーをそのまま返すことができます。
エラーの propagate を利用する
関数で発生したエラーは自動的に呼び出し元に propagate されます。
なので、関数の呼び出しに .catch がなければ、エラーは sub() のエラーとして呼び出し元の main() に返されます。
エラーをキャッチして行いたい処理がなければ .catch を書かないことで呼び出し元にエラーを返すことができます。
async function sub(): Promise<void> {
console.log('await の前だよ')
const result
= await asyncFuncToReturnString(true) // ここでエラーが返される
console.log('await の後だよ')
console.log(result)
return
}
実行結果
await の前だよ
catch: something went wrong!
キャッチしたエラーを throw する
.catch でキャッチしたエラーを throw します。
そうすることで throw より下の行の実行をキャンセルして、呼び出し元にエラーを返すことができます。
async function sub(): Promise<void> {
const result = await asyncFuncToReturnString(true)
.catch((e) => {
console.log('catch の中だよ')
console.log(e)
console.log('throw の前だよ')
throw e // ここでエラーが返される
console.log('throw の後だよ')
})
console.log('catch の外だよ')
console.log(result)
return
}
実行結果
catch の中だよ
something went wrong!
throw の前だよ
catch: something went wrong!
Promise.reject() を return する
.catch の中で Promise.reject() を return します。
そうすることで reruen より下の行の実行をキャンセルして、呼び出し元にエラーを返すことができます。
async function sub(): Promise<void> {
const result = await asyncFuncToReturnString(true)
.catch((e) => {
console.log('catch の中だよ')
console.log(e)
console.log('return の前だよ')
return Promise.reject(e) // ここでエラーが返される
console.log('return の後だよ')
})
console.log('catch の外だよ')
console.log(result)
return
}
実行結果
catch の中だよ
something went wrong!
return の前だよ
catch: something went wrong!
Promise.reject() を await する
.catch の中で Promise.reject() を await します。
そうすることで await より下の行の実行をキャンセルして、呼び出し元にエラーを返すことができます。
async function sub(): Promise<void> {
const result = await asyncFuncToReturnString(true)
.catch(async (e) => {
console.log('catch の中だよ')
console.log(e)
console.log('await の前だよ')
await Promise.reject(e) // ここでエラーが返される
console.log('await の後だよ')
})
console.log('catch の外だよ')
console.log(result)
return
}
実行結果
catch の中だよ
something went wrong!
await の前だよ
catch: something went wrong!
おわりに
await/catch パターンでも工夫をすればアーリーリターンができることが分かりました。
ところで、記事を書くときに "アーリーリターン" と "early return" とか、エラーの "伝播" と "propagate" とか、どう記述するか迷ったのですがどっちが良いんでしょう。