Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion NativeScript/runtime/Interop.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1523,7 +1523,38 @@ inline bool isBool() {
NSError* error = errorPtr[0];
std::free(errorRef);
if (error) {
throw NativeScriptException([[error localizedDescription] UTF8String]);
// Create JS Error with localizedDescription, attach code, domain and nativeException,
// and throw it into V8 so JS catch handlers receive it (with proper stack).
Isolate* isolate = methodCall.context_->GetIsolate();
Local<Context> context = isolate->GetCurrentContext();

Local<Value> jsErrVal = Exception::Error(tns::ToV8String(isolate, [[error localizedDescription] UTF8String]));
if (jsErrVal.IsEmpty() || !jsErrVal->IsObject()) {
// Fallback: if for some reason we cannot create an Error object, throw a generic NativeScriptException
throw NativeScriptException([[error localizedDescription] UTF8String]);
}

Local<Object> jsErrObj = jsErrVal.As<Object>();

// Attach the NSError code (number) and domain (string)
jsErrObj->Set(context, tns::ToV8String(isolate, "code"), Number::New(isolate, (double)[error code])).FromMaybe(false);
if (error.domain) {
jsErrObj->Set(context, tns::ToV8String(isolate, "domain"), tns::ToV8String(isolate, [error.domain UTF8String])).FromMaybe(false);
} else {
jsErrObj->Set(context, tns::ToV8String(isolate, "domain"), Null(isolate)).FromMaybe(false);
}

// Wrap the native NSError instance into a JS object and attach as nativeException
ObjCDataWrapper* wrapper = new ObjCDataWrapper(error);
Local<Value> nativeWrapper = ArgConverter::CreateJsWrapper(context, wrapper, Local<Object>(), true);
Comment on lines +1548 to +1549
Copy link

Copilot AI Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ObjCDataWrapper is created with skipGCRegistration=true in CreateJsWrapper, but there's no explicit cleanup if the Error object creation or subsequent Set operations fail. Consider using a smart pointer or ensuring the wrapper is properly cleaned up in the error path to prevent memory leaks.

Copilot uses AI. Check for mistakes.
jsErrObj->Set(context, tns::ToV8String(isolate, "nativeException"), nativeWrapper).FromMaybe(false);

// Ensure the Error has a proper 'name' property.
jsErrObj->Set(context, tns::ToV8String(isolate, "name"), tns::ToV8String(isolate, "NSError")).FromMaybe(false);

// Throw the JS Error with full stack information — V8 will populate the stack for the created Error object.
isolate->ThrowException(jsErrObj);
return Local<Value>();
}
}

Expand Down
46 changes: 46 additions & 0 deletions TestRunner/app/tests/ApiTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,52 @@ describe(module.id, function () {
JSApi.new().methodError(1);
}).toThrowError(/JS error/);
});
it("throws JS Error wrapping NSError when no error arg is passed", function () {
var isThrown = false;
try {
// TNSApi.methodError(errorCode, error: NSError**)
// Calling without the last interop.Reference should cause the runtime to
// throw a JS Error that wraps the native NSError (for non-zero errorCode).
TNSApi.new().methodError(1);
} catch (e) {
isThrown = true;

// Basic shape checks
expect(e).toBeDefined();
expect(e.message).toEqual(jasmine.any(String));
expect(e.stack).toEqual(jasmine.any(String)); // proper JS stack present

// Fields we attach from the NSError
expect(e.code).toBe(1);
expect(e.domain).toBe("TNSErrorDomain");

// nativeException should be the wrapped NSError object
expect(e.nativeException).toBeDefined();
// The wrapped object should behave like an NSError proxy/wrapper
// (we assert existence of localizedDescription property)
expect(typeof e.nativeException.localizedDescription).toBe('string');
} finally {
expect(isThrown).toBe(true);
}
});

it("does not throw when error arg is passed and the error ref is filled", function () {
// When the caller passes an interop.Reference() as the last argument,
// the runtime should not throw; it should return the method's boolean
// result and write the NSError into the reference.
var errorRef = new interop.Reference();
var result = TNSApi.new().methodError(1, errorRef);

// The method returns false for non-zero error code
expect(result).toBe(false);

// The errorRef should be populated with an NSError
expect(errorRef.value instanceof NSError).toBe(true);

// Validate the NSError contents
expect(errorRef.value.code).toBe(1);
expect(errorRef.value.domain).toBe("TNSErrorDomain");
});

// it("NSErrorExpose", function () {
// var JSApi = TNSApi.extend({
Expand Down