SwiftのbeginAsyncにthrowsが要らない理由

[iOSDC 2018 Japan](https://iosdc.jp/2018/) の一昨日の @yimajo さんのセッション、 ["Swiftの生みの親によるasync/await for Swiftを徹底解説し、新しい非同期処理の手法を理解する"](https://fortee.jp/iosdc-japan-2018/proposal/da947677-59a1-4843-a3dd-5afcdf2d74b5) で「 `beginAsync` の引数に渡すクロージャの型が `() async throws -> Void` だけれども、この `throws` はない方がいいんじゃないか」という趣旨の質問をしました。その意図について説明したいと思います。

## `beginAsync` って何？

`beginAsync` は Swift の `async/await` の[プロポーザルドラフト](https://gist.github.com/lattner/429b9070918248274f25b714dcfc7619)に中で[提案されている](https://gist.github.com/lattner/429b9070918248274f25b714dcfc7619#entering-and-leaving-async-code)関数です。プロポーザルが通れば Swift の標準ライブラリに追加されます。

`beginAsync` のシグネチャは次の通りです。

```swift
func beginAsync(_ body: () async throws -> Void) rethrows -> Void
```

`throws` な関数は `throws` な関数の中でしか使えない（か `do/catch` で囲まないといけない）のと同じように、 `async` な関数は `async` な関数の中でしか使えません。しかし、そうすると最初に非同期処理を開始する箇所はどうすれば良いでしょうか。 `main` 関数まで `async` にしないといけなくなってしまいます。 `async` で **ない** の中で `async` な関数を開始するための関数が `beginAsync` です。

たとえば、次のような `download` 関数を考えてみます。

```swift
func download(from url: URL) async -> Data
```

ダウンロードは非同期処理なので `async` が付与されています。ボタンを押したらダウンロードを開始するという処理の場合、次のように書きたくなります。（ `throws` な関数を呼ぶのに `try` の付与が必須なように、 `async` な関数を呼びのには `await` の付与が必須です。）

```swift
@IBAction func pressButton(_ sender: UIButton) {
    self.data = await download(from: url)
}
```

しかし、 `pressButton` は `async` でないただのメソッドなので、このメソッドの中で直接 `download` を呼びことはできません。そこで `beginAsync` を使って次のように書きます。

```swift
@IBAction func pressButton(_ sender: UIButton) {
    beginAsync {
        self.data = await download(from: self.url)
    }
}
```

`beginAsync` に渡されるクロージャの型は `() async throws -> Void` なので、このクロージャ式の中では `async` な関数をコールできるわけです。

## 何が問題なの？

前述の通り、 `beginAsync` のシグネチャは次のようになっていますが、

```swift
func beginAsync(_ body: () async throws -> Void) rethrows -> Void
```

僕は、↓であるべきだと考えています。

```swift
func beginAsync(_ body: () async -> Void) -> Void
```

一見、 `throws` が付与されているから非同期エラー処理ができるように見えるんですが、 **そんなことはありません** 。

たとえば、次のようなコードを考えてみましょう。

```swift
func foo() {
    do {
        beginAsync {
            let data = await download(self.url)
            try data.write(to: pathURL) // ここでエラーが発生したらどうなる？
        }
    } catch let error {
        // エラー処理
    }
}
```

もし `try data.write(to: pathURL)` でエラーが発生した場合、外の `catch` でエラー処理することはできるでしょうか？

この `foo` が実行された場合、 `beginAsync` が呼ばれると、 `beginAsync` に渡されたクロージャに同期的に突入します。なので、 `beginAsync` のコールが終わる前に `await download(self.url)` が実行されます。

しかし、ここで `await` があるので、この後の処理は非同期化されます。同じスレッドで実行される保証すらありません。そのため、 `download` が呼ばれた直後に `beginAsync` を抜けて、当然この時点ではエラーは発生していないので `catch` 節に入ることもなく、そのまま `foo` を抜けることになります。

その後、ダウンロード処理が完了すると結果が `data` に代入され、ようやく `try data.write(to: pathURL)` が実行されます。ここでエラーが発生したとしても、もう `foo` の `do/catch` はとっくに抜けた後です。 `beginAsync` の外側にエラーを rethrow することはできません。結果、発生したエラーは黙殺するか、クラッシュさせるしかなくなります。コンパイル時に静的に検査できない危険なコードになってしまいました。

もし、↓のように `throws` がなければどうなるでしょうか？

```swift
func beginAsync(_ body: () async -> Void) -> Void
```

`beginAsync` には `throws` なクロージャを渡せなくなるので、クロージャの中で `throws` な関数をコールすることもできなくなります。そのため、次のようなコードはコンパイルエラーとなります。

```swift
beginAsync {
    let data = await download(self.url)
    try data.write(to: pathURL) // try できないのでコンパイルエラー
}
```

もしエラー処理をしたければ次のように書くことになります。

```swift
beginAsync {
    do {
        let data = await download(self.url)
        try data.write(to: pathURL) // `do/catch` の中なので OK
    } catch let error {
        // エラー処理
    }
}
```

これなら `beginAsync` の中でエラー処理が完結しているので OK です。コンパイルエラーによって正しくエラー処理を促すことができました。素晴らしいですね！

というわけで、 `throws` 要らないんじゃないかということでした。いかがでしょうか？