Mike Kienenberger
02/20/2026, 4:15 PMjsdom via global-jsdom.
We finally determined that the reason we have tests which never complete in certain repeatable circumstances was that the default coroutines dispatcher was being set to window.asCoroutineDispatcher() rather than NodeDispatcher().
There's currently code in kotlinx-coroutines-core/js/src/CoroutineContext.kt which tries to detect if jsdom is in use by looking for a user-agent containing "`jsdom`".
We would prefer a way to specify the default dispatcher type without modifying the user-agent since this is requiring specific knowledge of how coroutines works behind the scenes, and it's already changed once from "react-native" to "jsdom". It also seems like the approach in general is hit-or-miss and won't scale well as other situations like mine occur in the future.
One approach we used to verify this was our issue was to define a dispatcher override function as well as a function to specifically pick the internal NodeDispatcher.[1]
This did work, but does require that we call setDispatcherOverrideToNodeDispatcher() before any other code triggers the creation of the default dispatcher.
It might be better to instead look for a flag or value in a known location (globalThis.kotlinx.coroutines.config.defaultdispatcher?) which would be set in the environment before kotlin starts executing code. I'm not much of a javascript expert, though.
For now, we are manually appending " jsdom" to the current user-agent rather than maintain our own fork of coroutines.
Object.defineProperty(globalThis.navigator, "userAgent", {
value: (globalThis.navigator?.userAgent ?? "Node.js") + " jsdom",
configurable: true
});
[1] Sample code which provided a hook to change the default dispatcher to NodeTest
condensed diff --git a/kotlinx-coroutines-core/js/src/CoroutineContext.kt b/kotlinx-coroutines-core/js/src/CoroutineContext.kt
-internal actual fun createDefaultDispatcher(): CoroutineDispatcher = when {
+internal actual fun createDefaultDispatcher(): CoroutineDispatcher {
+ dispatcherOverride?.let { return it() }
+
+ return when {
@@ -18,6 +21,7 @@ i
+ }
}
@@ -26,3 +30,13 @@
+
+internal var dispatcherOverride: (() -> CoroutineDispatcher)? = null
+
+public fun setDispatcherOverride(block: (() -> CoroutineDispatcher)?) {
+ dispatcherOverride = block
+}
+
+public fun setDispatcherOverrideToNodeDispatcher() {
+ dispatcherOverride = { NodeDispatcher }
+}Oliver.O
02/20/2026, 4:28 PMMike Kienenberger
02/20/2026, 5:25 PMOliver.O
02/20/2026, 5:27 PM