Kotlin Coroutineの悪い例と解決策

昨日、Coroutineを理解するために以下の記事を書いた

Kotlin Coroutinesパターン＆アンチパターン - Qiita 
https://qiita.com/ikemura23/items/fb8caeba4c35fcd85644

ただ書いただけではなく、本当にそうなのか実装して確かめてみた。

## 例外処理をtry/catchに頼らない

エラーが発生する`doWork()`をtry / catchしてもダメ

```kotlin
class MainActivity : AppCompatActivity() {

    private val job = Job()
    private val scope = CoroutineScope(Dispatchers.Default + job)

    override fun onResume() {
        super.onResume()

        // Throw Exceptionの可能性がある処理
        fun doWork(): Deferred<String> = scope.async { throw UnknownError() }

        scope.launch {
            try {
                doWork().await() // ここで落ちる
            } catch (e: Exception) {
                Log.d(TAG, "ここに到達しない")
            }
        }
    }
```

### 結果

実行すると、trt / catchの意味なくエラーでアプリが落ちる

```
E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
    Process: com.ikemura.android_kotlin_lab, PID: 9287
    java.util.concurrent.TimeoutException
        ...
```

### 解決策

`SupervisorJob`で子のエラーを回避する。

```kotlin

class MainActivity : AppCompatActivity() {

    private val job = SupervisorJob() // <= SupervisorJobに変更
    private val scope = CoroutineScope(Dispatchers.Default + job)

    override fun onResume() {
        super.onResume()

        // Throw Exceptionの可能性がある処理
        fun doWork(): Deferred<String> = scope.async { throw UnknownError() }

        scope.launch {
            try {
                doWork().await() // ここで落ちる
            } catch (e: Exception) {
                Log.d(TAG, "キャッチされる")
            }
        }
    }
```

### 注意すべきasync
↑で示した解決は、SupervisorJobを使用してCoroutineScopeで非同期を明示的に実行した場合にのみ機能する。

以下の async は`SupervisorJob`を使用してもエラーとなる。

```kotlin
    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.Default + job)

    // エラーの可能性がある処理
    fun doWork(): Deferred<String> = throw TimeoutException()

    fun loadData() = scope.launch {
        try {
            async {
                doWork() // ここで落ちる
            }
        } catch (e: Exception) {
            Log.d(TAG, "キャッチされない")
        }
    }
```

理由は、async内で別のcoroutineが作成され、その範囲内で起動されるから。（ちょっと曖昧）

### 解決策
coroutineScopeを使ってasyncをラップする。

```kotlin
    // エラーの可能性がある処理
    fun doWork(): Deferred<String> = throw TimeoutException()

    fun loadData() {

        scope.launch {
            try {
                coroutineScope { // 追加
                    async {
                        doWork()
                    }
                }
                Log.d(TAG, "ここは実行されない")
            } catch (e: Exception) {
                Log.d(TAG, "キャッチされる")
            }
        }
    }
```
上記コードは分かりにくいので、こう見やすくできる。

```kotlin
    // エラーの可能性がある処理
    suspend fun doWork(): Deferred<String> = coroutineScope {
        async { throw TimeoutException() }
    }

    fun loadData() {
        scope.launch {
            try {
                doWork() // coroutineScopeとasyncをdoWorkに移動させた
                Log.d(TAG, "ここは実行されない")
            } catch (e: Exception) {
                Log.d(TAG, "キャッチされる")
            }
        }
    }
```

## try/catchばかり使わない

SupervisorJobを作り、try/catchでラップしておけば大概のエラーはキャッチできるはず。
だが毎回try/catchを使うのではなく、コルーチンの例外をキャッチしてくれる[CoroutineExceptionHandler](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/)が用意されているので、こちらを使う。

### CoroutineExceptionHandlerを個別に指定する例

```kotlin
    // エラーハンドリングを作成
    private val exceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
        Log.e(TAG, "例外キャッチ $throwable") // ログ出力のみ
    }
    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.Default + job)

    // エラーの可能性がある処理
    fun doWork(): Deferred<String> = throw TimeoutException()

    fun loadData() {
        scope.launch(exceptionHandler) { // エラーハンドリングを指定
            doWork()
        }
    }
```
ログ

```
E/MainActivity: 例外キャッチ java.util.concurrent.TimeoutException
```

try/catchがなくなり、スッキリした。

### CoroutineExceptionHandlerを共通で使い回す

CoroutineExceptionHandlerを個別に指定するのもいいが、CoroutineScopeに渡せば共通で使い回せる。

```kotlin
private val exceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
        Log.e(TAG, "例外キャッチ $throwable")
    }
    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.Default + job + exceptionHandler) // exceptionHandlerを渡す

    // エラーの可能性がある処理
    fun doWork(): Deferred<String> = throw TimeoutException()

    fun loadData() {
        scope.launch { // exceptionHandler指定が無くなった
            doWork()
            Log.d(TAG, "到達しない")
        }
    }
```

普段は共通でしておき、必要な時だけ個別に例外を指定するのがベストだろう。

## 実装したコード
https://github.com/ikemura23/Android-Kotlin-Lab/tree/coroutine-sample

## 参考リンク
https://proandroiddev.com/kotlin-coroutines-patterns-anti-patterns-f9d12984c68e