From fe8eb341aa59505afa2be528887129227120b9b6 Mon Sep 17 00:00:00 2001 From: Martin Guillon Date: Sun, 2 Nov 2025 10:24:45 +0100 Subject: [PATCH 1/3] fix: native methods expecting a NSError arg will now throw a JS exception if the error arg is not passed --- NativeScript/runtime/Interop.mm | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/NativeScript/runtime/Interop.mm b/NativeScript/runtime/Interop.mm index d3973128..3e5edf22 100644 --- a/NativeScript/runtime/Interop.mm +++ b/NativeScript/runtime/Interop.mm @@ -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 = isolate->GetCurrentContext(); + + Local 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 jsErrObj = jsErrVal.As(); + + // 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 nativeWrapper = ArgConverter::CreateJsWrapper(context, wrapper, Local(), true); + 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(); } } From 20bd1ba9a2bd716ee8e79c658a83f66548b5f57b Mon Sep 17 00:00:00 2001 From: Martin Guillon Date: Sun, 2 Nov 2025 10:43:30 +0100 Subject: [PATCH 2/3] chore: added tests --- TestRunner/app/tests/ApiTests.js | 46 ++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/TestRunner/app/tests/ApiTests.js b/TestRunner/app/tests/ApiTests.js index 68ee5594..53860414 100644 --- a/TestRunner/app/tests/ApiTests.js +++ b/TestRunner/app/tests/ApiTests.js @@ -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 === "string" || e.nativeException.localizedDescription instanceof String).toBe(true); + } 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({ From dc9adaada24559d2c474ef4c83e4782137434c5d Mon Sep 17 00:00:00 2001 From: farfromrefuge Date: Tue, 4 Nov 2025 09:54:26 +0100 Subject: [PATCH 3/3] Update TestRunner/app/tests/ApiTests.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- TestRunner/app/tests/ApiTests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TestRunner/app/tests/ApiTests.js b/TestRunner/app/tests/ApiTests.js index 53860414..62aa337a 100644 --- a/TestRunner/app/tests/ApiTests.js +++ b/TestRunner/app/tests/ApiTests.js @@ -337,7 +337,7 @@ describe(module.id, function () { 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 === "string" || e.nativeException.localizedDescription instanceof String).toBe(true); + expect(typeof e.nativeException.localizedDescription).toBe('string'); } finally { expect(isThrown).toBe(true); }