web-dev-qa-db-fra.com

Kotlin Coroutine ne peut pas gérer l'exception

Je jouais avec des coroutines et j'ai trouvé un comportement très étrange. Je souhaite convertir certaines demandes asynchrones dans mon projet à l'aide de suspendCoroutine(). Voici un morceau de code montrant ce problème.

Dans le premier cas, lorsque la fonction suspend est appelée dans runBlocking coroutine, l'exception de la continuation va au bloc catch, puis runBlocking se termine avec succès. Mais dans le deuxième cas, lors de la création d'une nouvelle coroutine async, une exception passe par le bloc catch et bloque tout le programme.

package com.example.lib

import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

object Test {
    fun runSuccessfulCoroutine() {
        runBlocking {
            try {
                Repository.fail()
            } catch (ex: Throwable) {
                println("Catching ex in runSuccessfulCoroutine(): $ex")
            }
        }
    }

    fun runFailingCoroutine() {
        runBlocking {
            try {
                async { Repository.fail() }.await()
            } catch (ex: Throwable) {
                println("Catching ex in runFailingCoroutine(): $ex")
            }
        }
    }
}

object Repository {
    suspend fun fail(): Int = suspendCoroutine { cont ->
        cont.resumeWithException(RuntimeException("Exception at ${Thread.currentThread().name}"))
    }
}


fun main() {
    Test.runSuccessfulCoroutine()
    println()

    Test.runFailingCoroutine()

    println("We will never get here")
}

C'est ce qui est imprimé sur la console:

Catching ex in runSuccessfulCoroutine(): Java.lang.RuntimeException: Exception at main

Catching ex in runFailingCoroutine(): Java.lang.RuntimeException: Exception at main
Exception in thread "main" Java.lang.RuntimeException: Exception at main
    at com.example.lib.Repository.fail(MyClass.kt:32)
    at com.example.lib.Test$runFailingCoroutine$1$1.invokeSuspend(MyClass.kt:22)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
    at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:236)
    at kotlinx.coroutines.EventLoopBase.processNextEvent(EventLoop.kt:123)
    at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:69)
    at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:45)
    at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
    at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:35)
    at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
    at com.example.lib.Test.runFailingCoroutine(MyClass.kt:20)
    at com.example.lib.MyClassKt.main(MyClass.kt:41)
    at com.example.lib.MyClassKt.main(MyClass.kt)

Process finished with exit code 1

Des idées pourquoi cela se produit - est-ce un bug, ou est-ce que j'utilise les coroutines dans le mauvais sens?

Mise à jour:

L'utilisation de coroutineScope { ... } Atténuera le problème dans runFailingCoroutine()

fun runFailingCoroutine() = runBlocking {
    try {
        coroutineScope { async { fail() }.await()  }
    } catch (ex: Throwable) {
        println("Catching ex in runFailingCoroutine(): $ex")
    }
}
7
Alexander Sitnikov

Le comportement de votre deuxième exemple est correct, c'est le travail de la concurrence structurée. Étant donné que le bloc async interne lève une exception, cette coroutine est annulée. En raison de la simultanéité structurée, le travail parent est également annulé.

Regardez ce petit exemple:

val result = coroutineScope {
    async {
        throw IllegalStateException()
    }
    10
}

Ce bloc ne renverra jamais de valeur, même si nous ne demandons jamais le résultat async. La coroutine intérieure est annulée et la portée extérieure est également annulée.

Si vous n'aimez pas ce comportement, vous pouvez utiliser le supervisorScope. Dans ce cas, la coroutine intérieure peut échouer sans faire échouer la coroutine extérieure.

val result = supervisorScope {
    async {
        throw IllegalStateException()
    }
    10
}

Dans votre premier exemple, vous interceptez l'exception à l'intérieur du bloc de coroutine, à cause de cela, la coroutine se ferme normalement.

Pour une discussion sur ce sujet, voir:

2
Rene

J'ai été frappé par ce comportement hier, voici mon analyse .

En bref, ce comportement est souhaité car async n'a pas le même objectif que dans d'autres langues. Dans Kotlin, vous devez l'utiliser avec parcimonie, uniquement lorsque vous devez décomposer une tâche en plusieurs sous-tâches qui s'exécutent en parallèle.

Chaque fois que vous voulez juste écrire

val result = async { work() }.await()

vous devriez plutôt écrire

val result = withContext(Default) { work() }

et cela se comportera comme prévu. De plus, chaque fois que vous en avez l'occasion, vous devez déplacer l'appel withContext dans la fonction work() et en faire un suspend fun.

3
Marko Topolnik