Skip to content

Propagate exception to wasm-js and js in propagateExceptionFinalResort #4472

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineConte
* with the corresponding exception when the handler is called. Normally, the handler is used to
* log the exception, show some kind of error message, terminate, and/or restart the application.
*
* If you need to handle exception in a specific part of the code, it is recommended to use `try`/`catch` around
* If you need to handle the exception in a specific part of the code, it is recommended to use `try`/`catch` around
* the corresponding code inside your coroutine. This way you can prevent completion of the coroutine
* with the exception (exception is now _caught_), retry the operation, and/or take other arbitrary actions:
*
Expand All @@ -83,14 +83,15 @@ public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineConte
*
* ### Uncaught exceptions with no handler
*
* When no handler is installed, exception are handled in the following way:
* - If exception is [CancellationException], it is ignored, as these exceptions are used to cancel coroutines.
* When no handler is installed, an exception is handled in the following way:
* - If the exception is [CancellationException], it is ignored, as these exceptions are used to cancel coroutines.
* - Otherwise, if there is a [Job] in the context, then [Job.cancel] is invoked.
* - Otherwise, as a last resort, the exception is processed in a platform-specific manner:
* - On JVM, all instances of [CoroutineExceptionHandler] found via [ServiceLoader], as well as
* the current thread's [Thread.uncaughtExceptionHandler], are invoked.
* - On Native, the whole application crashes with the exception.
* - On JS, the exception is logged via the Console API.
* - On JS and Wasm/JS, the exception is propagated into the event loop
* and is processed in a platform-specific way determined by the platform itself.
Comment on lines +93 to +94
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need some help with the wording, though. I'm unsure on how we spell Wasm/JS (because there's Wasm/WASI) (if Wasm/JS is ok, would the full spelling be Kotlin/Wasm/JS ? That doesn't look great)

Also, not sure saying "event loop" is a fair representation

*
* [CoroutineExceptionHandler] can be invoked from an arbitrary thread.
*/
Expand All @@ -102,7 +103,7 @@ public interface CoroutineExceptionHandler : CoroutineContext.Element {

/**
* Handles uncaught [exception] in the given [context]. It is invoked
* if coroutine has an uncaught exception.
* if the coroutine has an uncaught exception.
*/
public fun handleException(context: CoroutineContext, exception: Throwable)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ internal expect val platformExceptionHandlers: Collection<CoroutineExceptionHand
internal expect fun ensurePlatformExceptionHandlerLoaded(callback: CoroutineExceptionHandler)

/**
* The platform-dependent global exception handler, used so that the exception is logged at least *somewhere*.
* The platform-dependent global exception handler, used so that the exception is processed at least *somewhere*.
*/
internal expect fun propagateExceptionFinalResort(exception: Throwable)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package kotlinx.coroutines.internal

import kotlinx.coroutines.*
import kotlin.js.unsafeCast

internal actual fun propagateExceptionFinalResort(exception: Throwable) {
// log exception
console.error(exception.toString())
}
internal actual external interface JsAny

internal actual fun Throwable.toJsException(): JsAny = this.unsafeCast<JsAny>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package kotlinx.coroutines

import kotlinx.coroutines.testing.*
import kotlin.js.*
import kotlin.test.*

class PropagateExceptionFinalResortTest : TestBase() {
@BeforeTest
private fun removeListeners() {
// Remove a Node.js's internal listener, which prints the exception to stdout.
js("""
globalThis.originalListeners = process.listeners('uncaughtException');
process.removeAllListeners('uncaughtException');
""")
}

@AfterTest
private fun restoreListeners() {
js("""
if (globalThis.originalListeners) {
process.removeAllListeners('uncaughtException');
globalThis.originalListeners.forEach(function(listener) {
process.on('uncaughtException', listener);
});
}
""")
}

/*
* Test that `propagateExceptionFinalResort` re-throws the exception on JS.
*
* It is checked by setting up an exception handler within JS.
*/
@Test
fun testPropagateExceptionFinalResortReThrowsOnNodeJS() = runTest {
js("""
globalThis.exceptionCaught = false;
process.on('uncaughtException', function(e) {
globalThis.exceptionCaught = true;
});
""")
val job = GlobalScope.launch {
throw IllegalStateException("My ISE")
}
job.join()
delay(1) // Let the exception be re-thrown and handled.
val exceptionCaught = js("globalThis.exceptionCaught") as Boolean
assertTrue(exceptionCaught)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package kotlinx.coroutines.internal

import kotlinx.coroutines.*

internal expect interface JsAny

internal expect fun Throwable.toJsException(): JsAny

/*
* Schedule an exception to be thrown inside JS or Wasm/JS event loop,
* rather than in the current execution branch.
*/
internal fun throwAsync(e: JsAny): Unit = js("setTimeout(function () { throw e }, 0)")

internal actual fun propagateExceptionFinalResort(exception: Throwable) {
throwAsync(exception.toJsException())
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package kotlinx.coroutines.internal

import kotlinx.coroutines.*
internal actual typealias JsAny = kotlin.js.JsAny

internal actual fun propagateExceptionFinalResort(exception: Throwable) {
// log exception
console.error(exception.toString())
}
internal actual fun Throwable.toJsException(): JsAny =
toJsError(message, this::class.simpleName, stackTraceToString())

internal fun toJsError(message: String?, className: String?, stack: String?): JsAny {
js("""
const error = new Error();
error.message = message;
error.name = className;
error.stack = stack;
return error;
""")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package kotlinx.coroutines

import kotlinx.coroutines.testing.TestBase
import kotlin.test.*

class PropagateExceptionFinalResortTest : TestBase() {
@BeforeTest
private fun addUncaughtExceptionHandler() {
addUncaughtExceptionHandlerHelper()
}

@AfterTest
private fun removeHandler() {
removeHandlerHelper()
}

/*
* Test that `propagateExceptionFinalResort` re-throws the exception on Wasm/JS.
*
* It is checked by setting up an exception handler within Wasm/JS.
*/
@Test
fun testPropagateExceptionFinalResortReThrowsOnWasmJS() = runTest {
val job = GlobalScope.launch {
throw IllegalStateException("My ISE")
}
job.join()
delay(1) // Let the exception be re-thrown and handled.
assertTrue(exceptionCaught())
}
}

private fun addUncaughtExceptionHandlerHelper() {
js("""
globalThis.exceptionCaught = false;
globalThis.exceptionHandler = function(e) {
globalThis.exceptionCaught = true;
};
process.on('uncaughtException', globalThis.exceptionHandler);
""")
}

private fun removeHandlerHelper() {
js("""
process.removeListener('uncaughtException', globalThis.exceptionHandler);
""")
}

private fun exceptionCaught(): Boolean = js("globalThis.exceptionCaught")