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

## はじめに

Typescript を await/catch パターンで書いているとき、こんな感じでアーリーリターンをしたいことはないでしょうか？
一見するとこれでアーリーリターンができるように見えますが `return` で実行は止まりません。

```ts
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)
```

実行結果

```console
catch の中だよ
something went wrong!
return の前だよ
catch の外だよ
undefined
```

なぜなら `.catch` の中にある `return` は変数 `result` に値を返すからです。
変数 `result` は、上の例では何も代入されないので `undefined` になりましたが、例えば `return` を `return 'foobar'` に変えれば、`'foobar'` になります。
このようなとき、どうしたらアーリーリターンができるのかを調べました。

## 環境

```zsh
% tsc --version
Version 4.4.3
```

## 元にするコード

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

```ts: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()
```

実行結果

```console
catch の中だよ
something went wrong!
return の前だよ
catch の外だよ
undefined
finish
```

## アーリーリターンの実現方法

### try-catch を使用する

いきなり await/catch パターンではないのですが、これが一番オーソドックスな方法だと思うので紹介します。
await した関数を `try` 節で囲んで `catch` 節の中で `return` します。
このように一般的な try-catch の書き方をすればアーリーリターンができます。

```ts
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
}
```

実行結果

```console
catch の中だよ
something went wrong!
return の前だよ
finish
```

### エラーの有無で判断する

エラーを保持する変数 `err` を用意して `.catch` の中でキャッチしたエラーを代入します。
エラーが発生しなかった場合 `.catch` の中は実行されないので、変数 `err` は `undefined` になります。
なので `err` の値が `undefined` でないか否かを判別することでアーリーリターンが可能です。
if の比較は `(err != null)` で行ってももちろん OK です。

```ts
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
}
```

実行結果

```console
catch の中だよ
catch の外だよ
something went wrong!
return の前だよ
finish
```

### await した関数の戻り値で判断する

#### 関数が戻り値を返す場合

`.catch` の中に `return` を書かなければ (もしくは `return` のみ、または `return undefined` を書けば) エラーが発生した場合に変数 `result` が `undefined` になります。
なので `result` の値が `undefined` か否かを判別することでアーリーリターンが可能です。
if の比較は `(result == null)` で行ってももちろん OK です。

```ts
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
}
```

実行結果

```console
catch の中だよ
something went wrong!
catch の外だよ
return の前だよ
finish
```

#### 関数が戻り値を返さない場合

`.catch` の中でキャッチしたエラーを `return` すれば、エラーが発生した場合に変数 `result` にエラーが代入されます。
なので `result` の値が `undefined` でないか否かを判別することでアーリーリターンが可能です。
if の比較は `(result != null)` で行ってももちろん OK です。

```ts
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
}
```

実行結果

```console
catch の中だよ
catch の外だよ
something went wrong!
return の前だよ
finish
```

## アーリーリターンをせずエラーを返す場合

エラーが発生したときアーリーリターンをしない場合もあると思います。
そのような場合は、以下のようにすることで呼び出し元にエラーをそのまま返すことができます。

### エラーの propagate を利用する

関数で発生したエラーは自動的に呼び出し元に propagate されます。
なので、関数の呼び出しに `.catch` がなければ、エラーは `sub()` のエラーとして呼び出し元の `main()` に返されます。
エラーをキャッチして行いたい処理がなければ `.catch` を書かないことで呼び出し元にエラーを返すことができます。

```ts
async function sub(): Promise<void> {
    console.log('await の前だよ')
    const result
        = await asyncFuncToReturnString(true) // ここでエラーが返される
    console.log('await の後だよ')
    console.log(result)
    return
}
```

実行結果

```console
await の前だよ
catch: something went wrong!
```

### キャッチしたエラーを throw する

`.catch` でキャッチしたエラーを `throw` します。
そうすることで `throw` より下の行の実行をキャンセルして、呼び出し元にエラーを返すことができます。

```ts
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
}
```

実行結果

```console
catch の中だよ
something went wrong!
throw の前だよ
catch: something went wrong!
```

### Promise.reject() を return する

`.catch` の中で `Promise.reject()` を `return` します。
そうすることで `reruen` より下の行の実行をキャンセルして、呼び出し元にエラーを返すことができます。

```ts
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
}
```

実行結果

```console
catch の中だよ
something went wrong!
return の前だよ
catch: something went wrong!
```

### Promise.reject() を await する

`.catch` の中で `Promise.reject()` を `await` します。
そうすることで `await` より下の行の実行をキャンセルして、呼び出し元にエラーを返すことができます。

```ts
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
}
```

実行結果

```console
catch の中だよ
something went wrong!
await の前だよ
catch: something went wrong!
```

## おわりに

await/catch パターンでも工夫をすればアーリーリターンができることが分かりました。
ところで、記事を書くときに "アーリーリターン" と "early return" とか、エラーの "伝播" と "propagate" とか、どう記述するか迷ったのですがどっちが良いんでしょう。

## 参考文献

1. [async関数においてtry/catchではなくawait/catchパターンを活用する](https://qiita.com/akameco/items/cc73afcdb5ac5d0774bc)
2. [async/awaitを利用したコードのエラーハンドリング](https://qiita.com/kenshiroh/items/18b709702bcbb265c055)
3. [await/catch を使うときの early return](https://zenn.dev/tomon9086/articles/e695a4c89cb89ad08702)