From 92a5f3d4fd9c1e2572af033e0251c47e8659f0e1 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Fri, 3 Apr 2026 15:18:56 -0700 Subject: [PATCH 1/2] fix: throw JS exception instead of silent warning for disposed native object calls in debug mode --- NativeScript/runtime/ArgConverter.mm | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/NativeScript/runtime/ArgConverter.mm b/NativeScript/runtime/ArgConverter.mm index c4820212..4e6564be 100644 --- a/NativeScript/runtime/ArgConverter.mm +++ b/NativeScript/runtime/ArgConverter.mm @@ -1,6 +1,5 @@ #include #include -#include #include "ArgConverter.h" #include "NativeScriptException.h" #include "DictionaryAdapter.h" @@ -32,19 +31,16 @@ if (wrapper == nullptr) { // During fast view churn like HMR in development, JS objects can outlive their - // native wrappers briefly. In Debug, avoid a crash and just skip the native call. + // native wrappers briefly. In Debug, throw a catchable JS error instead of crashing. // In Release, assert so crash reporting can capture unexpected cases. if (RuntimeConfig.IsDebug) { const char* selectorStr = meta ? meta->selectorAsString() : ""; const char* jsNameStr = meta ? meta->jsName() : ""; const char* classNameStr = klass ? class_getName(klass) : ""; - // Suppress duplicate logs: only log once per class+selector for this process. - static std::unordered_set s_logged; - std::string key = std::string(classNameStr) + ":" + selectorStr; - if (s_logged.insert(key).second) { - Log(@"Note: ignore method on non-native receiver (class: %s, selector: %s, jsName: %s, args: %d). Common during HMR.", - classNameStr, selectorStr, jsNameStr, (int)args.Length()); - } + std::string errMsg = std::string("Cannot call method '") + jsNameStr + + "' on a disposed native object (class: " + classNameStr + + ", selector: " + selectorStr + "). This can happen during HMR or fast view churn."; + isolate->ThrowException(Exception::Error(tns::ToV8String(isolate, errMsg))); return v8::Undefined(isolate); } else { tns::Assert(false, isolate); @@ -69,13 +65,11 @@ const char* selectorStr = meta ? meta->selectorAsString() : ""; const char* jsNameStr = meta ? meta->jsName() : ""; const char* classNameStr = klass ? class_getName(klass) : ""; - // Suppress duplicate logs: only log once per class+selector for this process. - static std::unordered_set s_logged; - std::string key = std::string(classNameStr) + ":" + selectorStr; - if (s_logged.insert(key).second) { - Log(@"Note: ignore receiver wrapper type %d (class: %s, selector: %s, jsName: %s). Common during HMR.", - (int)wrapper->Type(), classNameStr, selectorStr, jsNameStr); - } + std::string errMsg = std::string("Unexpected receiver wrapper type ") + + std::to_string((int)wrapper->Type()) + " for method '" + jsNameStr + + "' (class: " + classNameStr + ", selector: " + selectorStr + + "). This can happen during HMR or fast view churn."; + isolate->ThrowException(Exception::Error(tns::ToV8String(isolate, errMsg))); return v8::Undefined(isolate); } else { tns::Assert(false, isolate); From 934da3dd3d3d73840e373b97a50b4adbb558dc3e Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sun, 5 Apr 2026 18:50:06 -0700 Subject: [PATCH 2/2] fix: use NativeScriptException for disposed native object errors in debug mode --- NativeScript/runtime/ArgConverter.mm | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/NativeScript/runtime/ArgConverter.mm b/NativeScript/runtime/ArgConverter.mm index 4e6564be..5d6adb15 100644 --- a/NativeScript/runtime/ArgConverter.mm +++ b/NativeScript/runtime/ArgConverter.mm @@ -40,8 +40,7 @@ std::string errMsg = std::string("Cannot call method '") + jsNameStr + "' on a disposed native object (class: " + classNameStr + ", selector: " + selectorStr + "). This can happen during HMR or fast view churn."; - isolate->ThrowException(Exception::Error(tns::ToV8String(isolate, errMsg))); - return v8::Undefined(isolate); + throw NativeScriptException(isolate, errMsg); } else { tns::Assert(false, isolate); } @@ -69,8 +68,7 @@ std::to_string((int)wrapper->Type()) + " for method '" + jsNameStr + "' (class: " + classNameStr + ", selector: " + selectorStr + "). This can happen during HMR or fast view churn."; - isolate->ThrowException(Exception::Error(tns::ToV8String(isolate, errMsg))); - return v8::Undefined(isolate); + throw NativeScriptException(isolate, errMsg); } else { tns::Assert(false, isolate); }