0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

アンチパターン

  • 以下のコードの出力結果はどうなるか
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 があると、当該コルーチンはキャンセルされているため中断から戻ってこず、後続処理が実行されない
  • 次のような場合はどうなるか
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" は出力されません。

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

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

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

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

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

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() によるコルーチン起動も、どちらも動作するようになりました。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?