Skip to content

Commit 217873b

Browse files
authored
fix: native methods expecting a NSError arg will now throw a JS exception if the error arg is not passed (#311)
1 parent c371b6c commit 217873b

File tree

2 files changed

+78
-1
lines changed

2 files changed

+78
-1
lines changed

NativeScript/runtime/Interop.mm

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1574,7 +1574,38 @@ inline bool isBool() {
15741574
NSError* error = errorPtr[0];
15751575
std::free(errorRef);
15761576
if (error) {
1577-
throw NativeScriptException([[error localizedDescription] UTF8String]);
1577+
// Create JS Error with localizedDescription, attach code, domain and nativeException,
1578+
// and throw it into V8 so JS catch handlers receive it (with proper stack).
1579+
Isolate* isolate = methodCall.context_->GetIsolate();
1580+
Local<Context> context = isolate->GetCurrentContext();
1581+
1582+
Local<Value> jsErrVal = Exception::Error(tns::ToV8String(isolate, [[error localizedDescription] UTF8String]));
1583+
if (jsErrVal.IsEmpty() || !jsErrVal->IsObject()) {
1584+
// Fallback: if for some reason we cannot create an Error object, throw a generic NativeScriptException
1585+
throw NativeScriptException([[error localizedDescription] UTF8String]);
1586+
}
1587+
1588+
Local<Object> jsErrObj = jsErrVal.As<Object>();
1589+
1590+
// Attach the NSError code (number) and domain (string)
1591+
jsErrObj->Set(context, tns::ToV8String(isolate, "code"), Number::New(isolate, (double)[error code])).FromMaybe(false);
1592+
if (error.domain) {
1593+
jsErrObj->Set(context, tns::ToV8String(isolate, "domain"), tns::ToV8String(isolate, [error.domain UTF8String])).FromMaybe(false);
1594+
} else {
1595+
jsErrObj->Set(context, tns::ToV8String(isolate, "domain"), Null(isolate)).FromMaybe(false);
1596+
}
1597+
1598+
// Wrap the native NSError instance into a JS object and attach as nativeException
1599+
ObjCDataWrapper* wrapper = new ObjCDataWrapper(error);
1600+
Local<Value> nativeWrapper = ArgConverter::CreateJsWrapper(context, wrapper, Local<Object>(), true);
1601+
jsErrObj->Set(context, tns::ToV8String(isolate, "nativeException"), nativeWrapper).FromMaybe(false);
1602+
1603+
// Ensure the Error has a proper 'name' property.
1604+
jsErrObj->Set(context, tns::ToV8String(isolate, "name"), tns::ToV8String(isolate, "NSError")).FromMaybe(false);
1605+
1606+
// Throw the JS Error with full stack information — V8 will populate the stack for the created Error object.
1607+
isolate->ThrowException(jsErrObj);
1608+
return Local<Value>();
15781609
}
15791610
}
15801611

TestRunner/app/tests/ApiTests.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,52 @@ describe(module.id, function () {
321321
JSApi.new().methodError(1);
322322
}).toThrowError(/JS error/);
323323
});
324+
it("throws JS Error wrapping NSError when no error arg is passed", function () {
325+
var isThrown = false;
326+
try {
327+
// TNSApi.methodError(errorCode, error: NSError**)
328+
// Calling without the last interop.Reference should cause the runtime to
329+
// throw a JS Error that wraps the native NSError (for non-zero errorCode).
330+
TNSApi.new().methodError(1);
331+
} catch (e) {
332+
isThrown = true;
333+
334+
// Basic shape checks
335+
expect(e).toBeDefined();
336+
expect(e.message).toEqual(jasmine.any(String));
337+
expect(e.stack).toEqual(jasmine.any(String)); // proper JS stack present
338+
339+
// Fields we attach from the NSError
340+
expect(e.code).toBe(1);
341+
expect(e.domain).toBe("TNSErrorDomain");
342+
343+
// nativeException should be the wrapped NSError object
344+
expect(e.nativeException).toBeDefined();
345+
// The wrapped object should behave like an NSError proxy/wrapper
346+
// (we assert existence of localizedDescription property)
347+
expect(typeof e.nativeException.localizedDescription).toBe('string');
348+
} finally {
349+
expect(isThrown).toBe(true);
350+
}
351+
});
352+
353+
it("does not throw when error arg is passed and the error ref is filled", function () {
354+
// When the caller passes an interop.Reference() as the last argument,
355+
// the runtime should not throw; it should return the method's boolean
356+
// result and write the NSError into the reference.
357+
var errorRef = new interop.Reference();
358+
var result = TNSApi.new().methodError(1, errorRef);
359+
360+
// The method returns false for non-zero error code
361+
expect(result).toBe(false);
362+
363+
// The errorRef should be populated with an NSError
364+
expect(errorRef.value instanceof NSError).toBe(true);
365+
366+
// Validate the NSError contents
367+
expect(errorRef.value.code).toBe(1);
368+
expect(errorRef.value.domain).toBe("TNSErrorDomain");
369+
});
324370

325371
// it("NSErrorExpose", function () {
326372
// var JSApi = TNSApi.extend({

0 commit comments

Comments
 (0)