Swift 6.2 `async`/`await` no longer works on WebAssembly with JS interop, crashes instantly on suspension

the JavaScript event loop Task Executor seems to crash instantly on suspension after upgrading to Swift 6.2.

heres’s a minimal reproducer:

static func main() async throws {
    print("Installing JavaScript event loop...")
    JavaScriptEventLoop.installGlobalExecutor()
    print("JavaScript event loop installed.")

    print("A")
    try await Task.sleep(nanoseconds: 10_000_000_000)
    print("B")
}
Installing JavaScript event loop...
JavaScript event loop installed.
A
Uncaught Error: exit with exit code 0
    WASIProcExit wasi.js:1
    proc_exit wasi.js:1
    main runtime.js:373
    instantiate instantiate.js:60
    init index.js:17
    async* main.ts:141

what seems to happen is the process exits immediately as soon as it suspends for the first time. the default Cooperative Task Executor still seems to work, but obviously you cannot do much beyond the simplest toy examples with that mode.

i tried both --swift-sdk 6.2-RELEASE-wasm32-unknown-wasip1-threads and --swift-sdk 6.2-RELEASE-wasm32-unknown-wasip1 and got the same result.

1 Like

installGlobalExecutor is not compatible with async entrypoints. See the diff in Entrypoint.swift here for WebGPUDemo: Support Embedded Swift in `WebGPUDemo` by MaxDesiatov · Pull Request #44 · swiftlang/swift-for-wasm-examples · GitHub

Two options are currently available:

  1. Compatible with Swift 6.2.0, but doesn't work in Embedded Swift: non-async entrypoint in @main type, requires JavaScriptEventLoop.installGlobalExecutor() call. Your async functions have to be scheduled from an unstructured Task.
  2. Compatible with both non-embedded and Embedded Swift, but requires development snapshots: async entrypoint in @main with typealias DefaultExecutorFactory = JavaScriptEventLoop at the top level, JavaScriptEventLoop.installGlobalExecutor() is not needed.

The reason is that default executors API did not land in 6.2.0 and Embedded Swift concurrency only works with that API (and requires a few other changes that weren't included in release/6.2).

1 Like

probably won’t work for me, as i need the TypeScript caller to be able to await on some setup to complete.

is there any way to get back the behavior that worked on 6.1, short of downgrading the toolchain to 6.1.2?

1 Like

I'd appreciate if you try the new default executors API in main and 6.2 development snapshots and file issues for all of the bugs you encounter, so that we get fixes out as early as possible.

1 Like

i tested with the 9-20-2025 snapshot, and it seemed to work!

what is the best workaround to await on setup in @main in 6.2, in the meantime?

unfortunately, this didn’t work for me at all. the code inside a (non-detached) child Task just never seems to run at all.

the code inside a detached child Task runs up until the first suspension point, which is usually a call to some @MainActor function, after which it never progresses. so that doesn’t work either. so i could not get any sort of pattern like this to work:

let (events, stream): (
    AsyncStream<(PlayerEvent, UInt64)>,
    AsyncStream<(PlayerEvent, UInt64)>.Continuation
) = AsyncStream<(PlayerEvent, UInt64)>.makeStream()

self.swift["start"] = .object(JSClosure.init {
    Task.detached {
        print("A")
        try await self.handle(events: events)
    }
})

even a simple

Task.detached {
    print("A")
    try await Task.sleep(nanoseconds: 1_000_000_000)
    print("C")
}

does not work.

i am at a total loss for how to proceed, other than by downgrading back to 6.1.2.

I observed there are some issues around global concurrency executor hooks, and I'm investigating them now.

1 Like