4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Typescript の await/catch パターンでアーリーリターンする

Posted at

はじめに

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 になりましたが、例えば returnreturn 'foobar' に変えれば、'foobar' になります。
このようなとき、どうしたらアーリーリターンができるのかを調べました。

環境

% tsc --version
Version 4.4.3

元にするコード

まず、アーリーリターンがされないコードを示します。
これに手を加えて、関数 sub() の中でアーリーリターンができるようにします。

main.ts
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 の中は実行されないので、変数 errundefined になります。
なので 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 を書けば) エラーが発生した場合に変数 resultundefined になります。
なので 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" とか、どう記述するか迷ったのですがどっちが良いんでしょう。

参考文献

  1. async関数においてtry/catchではなくawait/catchパターンを活用する
  2. async/awaitを利用したコードのエラーハンドリング
  3. await/catch を使うときの early return
4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?