Skip to content

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

Open
@francescotescari

Description

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 🤔

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions