BridgeJS: Support throws and async for closures - #766
Merged
Merged
Conversation
krodak
force-pushed
the
kr/closures-throws-async
branch
from
June 11, 2026 09:42
142d0c7 to
c020734
Compare
This was referenced Sep 9, 2026
krodak
added a commit
to PassiveLogic/JavaScriptKit
that referenced
this pull request
Sep 16, 2026
Generic exported functions and methods may now be throws(JSException),
matching what generic imports already supported:
@js public func pickOrThrow<T: BridgedSwiftGenericBridgeable>(_ value: T, _ fail: Bool) throws(JSException) -> T
The concrete entry thunk wraps the existential-opening call chain in the
same do/catch every throwing export uses — the exception crosses through
the _swift_js_throw side channel — and the open-chain helpers become
throws(JSException) so the typed error propagates without erasure. On
the JavaScript side the wrapper rethrows the exception right after the
wasm call and before lifting the result, so a thrown call reads nothing
back and leaves the shared value stack balanced; the wrappers emitted
through the thunk builder get this from the existing effects-driven
exception check, and the struct-instance method path emits the same
sequence explicitly.
async generic exports remain rejected with a diagnostic. There is no
compiler obstacle — the wasm32 typed-throws closure issues are already
worked around by the forced-capture emission (swiftwasm#760) and JSException
storage boxing (swiftwasm#766) — but promise settlement is per-type today: each
async export settles through a Promise_resolve_<type> helper paired
with a JS settle handler that lifts a concrete value. A generic result
needs a codec-driven settlement path (stash the call's codec with the
promise's settlers, settle through the value stack), which is its own
ABI addition and lands separately.
Runtime tests cover the happy path, the surfaced exception, and that a
thrown call leaves the stacks balanced for the next generic call.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Adds
throws(JSException)andasync(andasync throws(JSException)) support to BridgeJS-bridged closures, in both directions: closures Swift receives from JavaScript, and closures Swift hands to JavaScript. Previously closures were limited to synchronous, non-throwing signatures (#537).Async settlement reuses the
_bjs_makePromise+ per-typePromise_resolve_<mangled>/Promise_rejectmachinery introduced in #758, so every bridged return type is inherited for free, including the associated-value enums added in #764; no parallel mechanism is introduced.1. Effect-aware closure mangling. Closure ABI symbol names encode effects with the Swift ABI operators (
Yafor async,Kfor throws), so signatures that differ only by effects no longer collide.2. Throwing closures (both directions). A JavaScript callback's thrown
JSExceptionpropagates into Swift; a Swift closure's thrownJSExceptionis routed back to JavaScript. Onlythrows(JSException)is supported; plainthrowsis diagnosed.3. Async closures (both directions). A JS-to-Swift callback is awaited via
_bjs_awaitPromise; a Swift-to-JS closure returns aPromisesettled via_bjs_makePromise. TypeScript surfaces these as(args) => Promise<R>. Supported return types mirror async functions, including@JS struct, enums (associated-value included), andOptional/Array/Dictionarycompositions; the remaining unsupported async return types (protocols, namespace enums) 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.Commit structure
The work is split so the parts can be reviewed (or dropped) independently:
Commit 1 is the feature itself, self-contained.
Commit 2 adds a build-time warning for async throwing closures handed to JavaScript (introducing a warning severity to the BridgeJS diagnostics). It was written when the swiftlang/swift#89320 miscompile made that signature lose thrown errors at runtime; with commit 3 in place the warning no longer fires for a real risk, so this commit can be dropped, or kept if the warning-severity infrastructure is useful on its own.
Commit 3 works around swiftlang/swift#89320 by boxing
JSException's stored properties into a private final class, shrinking the struct to a single stored reference so it travels in the direct typed-error convention and the miscompiled indirect-error path is never taken. The public API is unchanged; the cost is one heap allocation per thrown exception, on the error path only. With it, the previously gated Swift-to-JSasync throwsreject assertions run unconditionally and pass, and theJSClosure.asyncreject path gains its previously missing test. Removal once swiftlang/swift#89715 ships is tracked in #767 and is a library-internal layout change, not a breaking API change. Original exploration: PassiveLogic#13. The codegen-level counterpart for zero-parameter async throwing exports landed in #760 (tracked for removal in #761).Tests
Codegen snapshots and end-to-end runtime round-trips (both directions, throwing and async, resolve and reject,
Void,@JS struct, associated-value enum returns, the release-race, and concurrent calls) pass.