kotlinx.coroutines icon indicating copy to clipboard operation
kotlinx.coroutines copied to clipboard

[K/JS] Continuation exception is unexpectedly propagated to the resuming coroutine

Open francescotescari opened this issue 1 year ago • 0 comments

Describe the bug

In Kotlin/JS, when resuming the main coroutine exceptionally, the exception is thrown is the resuming coroutine (even if it's in a different scope) and it's not propagated in the main coroutine scope. This happens with Kotlin/JS. Coroutines version 1.7.3. Kotlin version 1.9.21.

Provide a Reproducer

Create a basic Kotlin/JS project with the following main file:

import kotlinx.coroutines.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.resumeWithException

val mainContinuation = CompletableDeferred<Continuation<Unit>>()
val scope = CoroutineScope(EmptyCoroutineContext)

suspend fun main() {

    // Wait for the main coroutine to suspend and resume it exceptionally
    scope.launch {
        val continuation = mainContinuation.await()
        val exception = Exception("Test exception")
        try {
            continuation.resumeWithException(exception)
        } catch (unexpected: Throwable) {
            // This should get printed
            println("Unexpected exception: $unexpected")
        }
    }

    // Suspend the main coroutine 
    suspendCancellableCoroutine {
        mainContinuation.complete(it)
    }

}

What I expect: the main coroutine is resumed exceptionally and the exception ends up in the global uncaught handler, the resuming coroutine is not affected, as it is in a different scope.

What happens instead: the main coroutine is resumed exceptionally, but then the exception is propagated to the resumer coroutine and caught in the catch block, printing Unexpected exception: Exception: Test exception. The global uncaught exception handlers is not invoked, as the exception doesn't appear in the console as it normally does otherwise.

More details: Catching the exception in the main coroutine prevents it from being propagated to the resumer. Wrapping the main suspending point in

    // Suspend the main coroutine
    try {
        suspendCancellableCoroutine {
            mainContinuation.complete(it)
        }
    } finally {
        delay(1)
    }

"fixes it" as the exception is no more propagated to the resumer and ends up in the uncaught handler.

If I understand coroutines, this is not the expected behavior, and it doesn't happen on JVM for example 🤔

francescotescari avatar Dec 07 '23 22:12 francescotescari