はじめに
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" とか、どう記述するか迷ったのですがどっちが良いんでしょう。