Kotlin Coroutines 例外Catch後にsuspend関数を呼ぶ場合

# はじめに

コルーチンの例外処理でsuspend関数を呼ぶ場合のアンチパターンを紹介します。

# アンチパターン

- 以下のコードの出力結果はどうなるか

``` kotlin
suspend fun main() {
    val scope = CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, _ -> println("A")})

    scope.launch {
        val deferred = async<Unit> {
            delay(100)
            throw MyException()
        }

        try {
            val result = deferred.await()
        } catch (_: MyException) {
            Thread.sleep(100)
            println("B")
            delay(100)
            println("C")
        }

        println("D")
    }

    delay(500) // main関数の終了を遅らせる
}
```

結果は、

```
B
A
```

注意しなければならない点
1. SupervisorJobを用いても、子コルーチンは孫コルーチンの例外に対してキャンセルが伝播する
2. `join()` や `await()`などのコルーチン完了の待機を try-catch すると、例外をキャッチできるが、キャンセル伝播を止めることはできない
3. catch ブロック内で suspend があると、当該コルーチンはキャンセルされているため中断から戻ってこず、後続処理が実行されない

- 次のような場合はどうなるか

``` kotlin
suspend fun main() {
    val scope = CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, _ -> println("A")})

    scope.launch {
        val deferred1 = async<Unit> {
            delay(300)
            throw MyException()
        }
        try {
            val result = deferred1.await()
        } catch (_: MyException) {
            println("B")
            val job = launch {
                println("C")
            }
            job.invokeOnCompletion { println(it) }
        }

        println("D")
    }

    delay(500) // main関数の終了を遅らせる
}
```

結果は、

```
B
D
kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=StandaloneCoroutine{Cancelling}@6ba26c3c
A
```

catch ブロック内に suspend が無いため、そのまま後続処理に進んで "D" が出力されました。しかし、catch 内の `lauch()` 関数によるコルーチン起動は、「Parent job is Cancelling」の理由で即座にキャンセルされてしまい "C" は出力されません。

# 例外キャッチ後に後続処理を行うためには

親コルーチンへ例外伝播しないようにする必要があります。

【方法１】async 内で例外キャッチする

``` kotlin
suspend fun main() {
    val scope = CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, _ -> println("A")})

    scope.launch {
        val deferred = async<Unit> {
            try {
                delay(300)
                throw MyException()
            } catch (_: MyException) {
                println("B")
                delay(100)
                val job = launch {
                    println("C")
                }
                job.invokeOnCompletion { println(it) }
            }
        }
        val result = deferred.await()

        println("D")
    }

    delay(500)
}
```
```
B
C
null
D
```

【方法２】supervisorScope() 関数を使用する

``` kotlin
suspend fun main() {
    val scope = CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, _ -> println("A")})

    scope.launch {
        val deferred = supervisorScope {
            async<Unit> {
                delay(300)
                throw MyException()
            }
        }

        try {
            val result = deferred.await()
        } catch (_: MyException) {
            println("B")
            delay(100)
            val job = launch {
                println("C")
            }
            job.invokeOnCompletion { println(it) }
        }

        println("D")
    }

    delay(500)
}
```
```
B
C
null
D
```

`delay()` による suspend や、`launch()` によるコルーチン起動も、どちらも動作するようになりました。