Skip to content

Commit 75b8cd3

Browse files
Concurrency: Introduce JSException and remove Error conformance from JSValue
This is a breaking change. It introduces a new `JSException` type to represent exceptions thrown from JavaScript code. This change is necessary to remove `Sendable` conformance from `JSValue`, which is derived from `Error` conformance.
1 parent 97aad00 commit 75b8cd3

File tree

6 files changed

+51
-11
lines changed

6 files changed

+51
-11
lines changed

Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ public extension JSPromise {
218218
return JSValue.undefined
219219
},
220220
failure: {
221-
continuation.resume(throwing: $0)
221+
continuation.resume(throwing: JSException($0))
222222
return JSValue.undefined
223223
}
224224
)
@@ -227,7 +227,7 @@ public extension JSPromise {
227227
}
228228

229229
/// Wait for the promise to complete, returning its result or exception as a Result.
230-
var result: Result<JSValue, JSValue> {
230+
var result: Swift.Result<JSValue, JSException> {
231231
get async {
232232
await withUnsafeContinuation { [self] continuation in
233233
self.then(
@@ -236,7 +236,7 @@ public extension JSPromise {
236236
return JSValue.undefined
237237
},
238238
failure: {
239-
continuation.resume(returning: .failure($0))
239+
continuation.resume(returning: .failure(JSException($0)))
240240
return JSValue.undefined
241241
}
242242
)

Sources/JavaScriptKit/BasicObjects/JSPromise.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,22 @@ public final class JSPromise: JSBridgedClass {
3131
return Self(jsObject)
3232
}
3333

34+
/// The result of a promise.
35+
public enum Result {
36+
/// The promise resolved with a value.
37+
case success(JSValue)
38+
/// The promise rejected with a value.
39+
case failure(JSValue)
40+
}
41+
3442
/// Creates a new `JSPromise` instance from a given `resolver` closure.
3543
/// The closure is passed a completion handler. Passing a successful
3644
/// `Result` to the completion handler will cause the promise to resolve
3745
/// with the corresponding value; passing a failure `Result` will cause the
3846
/// promise to reject with the corresponding value.
3947
/// Calling the completion handler more than once will have no effect
4048
/// (per the JavaScript specification).
41-
public convenience init(resolver: @escaping (@escaping (Result<JSValue, JSValue>) -> Void) -> Void) {
49+
public convenience init(resolver: @escaping (@escaping (Result) -> Void) -> Void) {
4250
let closure = JSOneshotClosure { arguments in
4351
// The arguments are always coming from the `Promise` constructor, so we should be
4452
// safe to assume their type here

Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ private func makeAsyncClosure(
144144
let result = try await context.body(context.arguments)
145145
context.resolver(.success(result))
146146
} catch {
147-
if let jsError = error as? JSError {
148-
context.resolver(.failure(jsError.jsValue))
147+
if let jsError = error as? JSException {
148+
context.resolver(.failure(jsError.thrownValue))
149149
} else {
150150
context.resolver(.failure(JSError(message: String(describing: error)).jsValue))
151151
}

Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public class JSThrowingFunction {
3737
/// - Parameter arguments: Arguments to be passed to this constructor function.
3838
/// - Returns: A new instance of this constructor.
3939
public func new(arguments: [ConvertibleToJSValue]) throws -> JSObject {
40-
try arguments.withRawJSValues { rawValues -> Result<JSObject, JSValue> in
40+
try arguments.withRawJSValues { rawValues -> Result<JSObject, JSException> in
4141
rawValues.withUnsafeBufferPointer { bufferPointer in
4242
let argv = bufferPointer.baseAddress
4343
let argc = bufferPointer.count
@@ -52,7 +52,7 @@ public class JSThrowingFunction {
5252
let exceptionKind = JavaScriptValueKindAndFlags(bitPattern: exceptionRawKind)
5353
if exceptionKind.isException {
5454
let exception = RawJSValue(kind: exceptionKind.kind, payload1: exceptionPayload1, payload2: exceptionPayload2)
55-
return .failure(exception.jsValue)
55+
return .failure(JSException(exception.jsValue))
5656
}
5757
return .success(JSObject(id: resultObj))
5858
}
@@ -92,7 +92,7 @@ private func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSV
9292
}
9393
}
9494
if isException {
95-
throw result
95+
throw JSException(result)
9696
}
9797
return result
9898
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/// `JSException` is a wrapper that handles exceptions thrown during JavaScript execution as Swift
2+
/// `Error` objects.
3+
/// When a JavaScript function throws an exception, it's wrapped as a `JSException` and propagated
4+
/// through Swift's error handling mechanism.
5+
///
6+
/// Example:
7+
/// ```swift
8+
/// do {
9+
/// try jsFunction.throws()
10+
/// } catch let error as JSException {
11+
/// // Access the value thrown from JavaScript
12+
/// let jsErrorValue = error.thrownValue
13+
/// }
14+
/// ```
15+
public struct JSException: Error {
16+
/// The value thrown from JavaScript.
17+
/// This can be any JavaScript value (error object, string, number, etc.).
18+
public var thrownValue: JSValue {
19+
return _thrownValue
20+
}
21+
22+
/// The actual JavaScript value that was thrown.
23+
///
24+
/// Marked as `nonisolated(unsafe)` to satisfy `Sendable` requirement
25+
/// from `Error` protocol.
26+
private nonisolated(unsafe) let _thrownValue: JSValue
27+
28+
/// Initializes a new JSException instance with a value thrown from JavaScript.
29+
///
30+
/// Only available within the package.
31+
package init(_ thrownValue: JSValue) {
32+
self._thrownValue = thrownValue
33+
}
34+
}

Sources/JavaScriptKit/JSValue.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,6 @@ public extension JSValue {
124124
}
125125
}
126126

127-
extension JSValue: Swift.Error {}
128-
129127
public extension JSValue {
130128
func fromJSValue<Type>() -> Type? where Type: ConstructibleFromJSValue {
131129
return Type.construct(from: self)

0 commit comments

Comments
 (0)