BridgeJS: Support throws and async for closures by krodak · Pull Request #11 · PassiveLogic/JavaScriptKit
Overview
Adds throws(JSException) and async (and async throws(JSException)) support to BridgeJS-bridged closures, in both directions: closures Swift receives from JavaScript, and closures Swift hands to JavaScript.
Async settlement reuses the _bjs_makePromise + per-type Promise_resolve_<mangled> / Promise_reject machinery introduced in swiftwasm#758, so every bridged return type (@JS struct, enums, Optional/Array/Dictionary) is inherited for free; no parallel mechanism is introduced.
1. Effect-aware closure mangling. Closure ABI symbol names encode effects with the Swift ABI operators (Ya for async, K for throws), so signatures that differ only by effects no longer collide.
2. Throwing closures (both directions). A JavaScript callback's thrown JSException propagates into Swift; a Swift closure's thrown JSException is routed back to JavaScript. Only throws(JSException) is supported; plain throws is diagnosed.
3. Async closures (both directions). A JS-to-Swift callback is awaited via _bjs_awaitPromise; a Swift-to-JS closure returns a Promise settled via _bjs_makePromise. TypeScript surfaces these as (args) => Promise<R>. Unsupported async return types (associated-value enums, protocols, namespace enums, including nested in Optional/Array/Dictionary) are diagnosed rather than miscompiled.
4. Lifetime. The closure box survives across suspension without an explicit pin: the extracted closure value, captured by the settling Task, keeps it alive even if JavaScript releases the closure mid-flight. Covered by release-race and concurrent-invocation tests.
Known limitation
A Swift-to-JS async throws(JSException) closure resolves correctly, but a thrown error does not currently propagate to JavaScript on the reject path. This is a Swift compiler bug on wasm32, swiftlang/swift#89320, with a fix in progress in swiftlang/swift#89715: when the typed error is too large for the direct error convention (as JSException is) and the closure value is captureless (or a function reference), IRGen and the wasm calling-convention padding disagree on parameter order at the thick call site, and the thrown error is lost across the async unwind.
The resolve path is unaffected, capturing closures are unaffected (a practical workaround), and JS-to-Swift async-throwing callbacks are unaffected. BridgeJS now emits a build-time warning when it sees the at-risk signature (a Swift-to-JS async throws(JSException) closure), pointing at swiftlang/swift#89320, so users are aware before they hit it at runtime. The reject assertions are gated with a pointer to swiftlang/swift#89320 and the limitation is documented in the closure articles. A stacked follow-up PR provides a standalone reproducer demonstrating the bug in isolation.
Tests
Codegen snapshots, the cross-swift-syntax-version matrix, and end-to-end runtime round-trips (both directions, throwing and async, resolve and reject, Void, @JS struct returns, the release-race, and concurrent calls) pass. The generated error.description lowering is consistent with swiftwasm#759.