From 2044ebfc8ce6e3e1ad7e9537604b8552f3b73c1b Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Thu, 23 Jul 2026 16:23:41 -0300 Subject: [PATCH 1/7] feat: web-compliant error handling (unhandled rejections, WHATWG error events) Ports the error model from NativeScript/ios#409 to the Android runtime. Unhandled promise rejection tracking: - Register SetPromiseRejectCallback (previously rejections vanished with no log, no callback). Rejections are tracked per-isolate and drained once per looper turn via a LooperTasks task, so a handler attached in the same turn always cancels the report. - Unhandled ones report through the existing __onUncaughtError hook with an always-on "Unhandled promise rejection:" logcat entry; late-attached handlers fire `rejectionhandled` (as a task, per spec, carrying the original reason; already-reported promises are held weakly). - Worker isolates dispatch `unhandledrejection` on the worker global first, then fall through to the existing worker onerror channel. WHATWG error events on globalThis: - Events.{h,cpp}: generic Event/EventTarget constructors, the addEventListener/removeEventListener/dispatchEvent mixin, and the internal backing target (stashed natively for future consumers). - ErrorEvents.{h,cpp}: ErrorEvent/PromiseRejectionEvent, reportError() and the native dispatch closures, layered on the generic primitives via a one-shot _installListenerErrorReporter handoff. Dispatch closures are captured at init so events keep firing even if app code overwrites globalThis.dispatchEvent. - NativeScriptException keeps the reporting machinery and the rejection tracker, mirroring the iOS file layout. preventDefault() semantics: - Uncaught errors dispatch a cancelable `error` ErrorEvent before the __onUncaughtError/__onDiscardedError hooks; preventDefault() fully handles the report. passExceptionToJsNative now returns that handled flag and NativeScriptUncaughtExceptionHandler skips the error activity and the default (crashing) handler when set. - Back-compat: unprevented errors behave exactly as before. Not ported (already native to Android): interop.escapeException - a JS throw in a method override already surfaces as a real com.tns.NativeScriptException to the Java caller, and caught Java exceptions already carry error.nativeException. crashOnUncaughtJsExceptions is likewise unnecessary - Android crashes by default on truly-uncaught exceptions. Testing: 30 new jasmine specs (WHATWG events/EventTarget semantics, reportError, unhandled rejections incl. rejectionhandled); full suite green on an API 35 arm64 emulator with no changes to existing suites. --- test-app/app/src/main/assets/app/mainpage.js | 2 + .../main/assets/app/tests/testErrorEvents.js | 245 +++++++++++++ .../app/tests/testUnhandledRejections.js | 181 ++++++++++ .../NativeScriptUncaughtExceptionHandler.java | 10 +- test-app/runtime/CMakeLists.txt | 2 + test-app/runtime/src/main/cpp/ErrorEvents.cpp | 244 +++++++++++++ test-app/runtime/src/main/cpp/ErrorEvents.h | 58 +++ test-app/runtime/src/main/cpp/Events.cpp | 181 ++++++++++ test-app/runtime/src/main/cpp/Events.h | 25 ++ .../src/main/cpp/NativeScriptException.cpp | 331 ++++++++++++++++++ .../src/main/cpp/NativeScriptException.h | 108 +++++- test-app/runtime/src/main/cpp/Runtime.cpp | 42 ++- test-app/runtime/src/main/cpp/Runtime.h | 51 ++- .../runtime/src/main/cpp/com_tns_Runtime.cpp | 7 +- .../src/main/java/com/tns/Runtime.java | 12 +- 15 files changed, 1480 insertions(+), 19 deletions(-) create mode 100644 test-app/app/src/main/assets/app/tests/testErrorEvents.js create mode 100644 test-app/app/src/main/assets/app/tests/testUnhandledRejections.js create mode 100644 test-app/runtime/src/main/cpp/ErrorEvents.cpp create mode 100644 test-app/runtime/src/main/cpp/ErrorEvents.h create mode 100644 test-app/runtime/src/main/cpp/Events.cpp create mode 100644 test-app/runtime/src/main/cpp/Events.h diff --git a/test-app/app/src/main/assets/app/mainpage.js b/test-app/app/src/main/assets/app/mainpage.js index 8184e7553..cf311f456 100644 --- a/test-app/app/src/main/assets/app/mainpage.js +++ b/test-app/app/src/main/assets/app/mainpage.js @@ -74,6 +74,8 @@ require('./tests/testURLImpl.js'); require('./tests/testURLSearchParamsImpl.js'); require('./tests/testPerformanceNow'); require('./tests/testQueueMicrotask'); +require('./tests/testErrorEvents'); +require('./tests/testUnhandledRejections'); require("./tests/testConcurrentAccess"); require("./tests/testESModules.mjs"); diff --git a/test-app/app/src/main/assets/app/tests/testErrorEvents.js b/test-app/app/src/main/assets/app/tests/testErrorEvents.js new file mode 100644 index 000000000..2e1cbd988 --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/testErrorEvents.js @@ -0,0 +1,245 @@ +describe("WHATWG error events", function () { + // Many tests exercise the global error path, which ends in the + // __onUncaughtError / __onDiscardedError hooks when a listener does not + // preventDefault(). Install spies for every test and restore the previous + // hooks in afterEach. Also track listeners added on the global target so + // they never leak into other suites (the internal EventTarget backing the + // global is process-wide). + var previousUncaughtHook; + var previousDiscardedHook; + var uncaught; + var discarded; + var addedGlobalListeners; + + beforeEach(function () { + previousUncaughtHook = global.__onUncaughtError; + previousDiscardedHook = global.__onDiscardedError; + uncaught = []; + discarded = []; + global.__onUncaughtError = function (error) { + uncaught.push(error); + }; + global.__onDiscardedError = function (error) { + discarded.push(error); + }; + addedGlobalListeners = []; + }); + + afterEach(function () { + global.__onUncaughtError = previousUncaughtHook; + global.__onDiscardedError = previousDiscardedHook; + for (var i = 0; i < addedGlobalListeners.length; i++) { + var l = addedGlobalListeners[i]; + global.removeEventListener(l.type, l.handler); + } + addedGlobalListeners = []; + }); + + function onGlobal(type, handler) { + global.addEventListener(type, handler); + addedGlobalListeners.push({ type: type, handler: handler }); + } + + // Wait a couple of quiet looper turns before asserting a NON-event. + function afterQuietTurns(cb) { + setTimeout(function () { + setTimeout(cb, 25); + }, 25); + } + + it("reportError fires an 'error' listener with an ErrorEvent carrying error and message", function (done) { + var err = new Error("x"); + var received = null; + onGlobal("error", function (e) { + received = e; + e.preventDefault(); + }); + + global.reportError(err); + + expect(received).not.toBeNull(); + expect(received instanceof ErrorEvent).toBe(true); + expect(received.type).toBe("error"); + expect(received.error).toBe(err); + expect(received.message).toBe("x"); + // preventDefault() in the listener must suppress the __onUncaughtError hook. + afterQuietTurns(function () { + expect(uncaught.length).toBe(0); + done(); + }); + }); + + it("reportError without preventDefault still invokes __onUncaughtError (back-compat)", function () { + var err = new Error("back-compat"); + var received = null; + onGlobal("error", function (e) { + received = e; + }); + + global.reportError(err); + + expect(received).not.toBeNull(); + expect(received.error).toBe(err); + expect(uncaught.length).toBe(1); + expect(uncaught[0]).toBe(err); + }); + + it("reportError throws TypeError when called with no arguments", function () { + expect(function () { + global.reportError(); + }).toThrowError(TypeError); + }); + + it("a discarded Java exception dispatches an 'error' event before __onDiscardedError", function () { + var received = null; + onGlobal("error", function (e) { + received = e; + }); + + var test = new com.tns.tests.DiscardedExceptionTest(); + test.reportSupressedException(); + + expect(received).not.toBeNull(); + expect(received instanceof ErrorEvent).toBe(true); + expect(received.error).not.toBeNull(); + expect(received.error.message).toBe("Exception to suppress"); + // Unprevented, so the existing hook still fires (back-compat). + expect(discarded.length).toBe(1); + expect(discarded[0]).toBe(received.error); + }); + + it("preventDefault() on the 'error' event suppresses __onDiscardedError", function () { + var received = null; + onGlobal("error", function (e) { + received = e; + e.preventDefault(); + }); + + var test = new com.tns.tests.DiscardedExceptionTest(); + test.reportSupressedException(); + + expect(received).not.toBeNull(); + expect(discarded.length).toBe(0); + }); + + describe("constructors and EventTarget semantics", function () { + it("Event is spec-sane and cancelable via preventDefault", function () { + var e = new Event("x", { cancelable: true }); + expect(e.type).toBe("x"); + expect(e.cancelable).toBe(true); + expect(e.bubbles).toBe(false); + expect(e.defaultPrevented).toBe(false); + e.preventDefault(); + expect(e.defaultPrevented).toBe(true); + }); + + it("a non-cancelable Event ignores preventDefault", function () { + var e = new Event("x"); + e.preventDefault(); + expect(e.defaultPrevented).toBe(false); + }); + + it("ErrorEvent exposes message/error/filename/lineno/colno", function () { + var err = new Error("boom"); + var e = new ErrorEvent("error", { message: "m", error: err }); + expect(e instanceof Event).toBe(true); + expect(e.message).toBe("m"); + expect(e.error).toBe(err); + expect(e.filename).toBe(""); + expect(e.lineno).toBe(0); + expect(e.colno).toBe(0); + }); + + it("PromiseRejectionEvent exposes promise/reason", function () { + var p = Promise.reject(1); + p.catch(function () {}); + var r = { some: "reason" }; + var e = new PromiseRejectionEvent("unhandledrejection", { promise: p, reason: r }); + expect(e instanceof Event).toBe(true); + expect(e.promise).toBe(p); + expect(e.reason).toBe(r); + }); + + it("dispatchEvent returns !defaultPrevented", function () { + var target = new EventTarget(); + target.addEventListener("t", function (e) { e.preventDefault(); }); + expect(target.dispatchEvent(new Event("t", { cancelable: true }))).toBe(false); + + var target2 = new EventTarget(); + target2.addEventListener("t", function () {}); + expect(target2.dispatchEvent(new Event("t", { cancelable: true }))).toBe(true); + }); + + it("once:true listener fires exactly once", function () { + var target = new EventTarget(); + var count = 0; + target.addEventListener("t", function () { count++; }, { once: true }); + target.dispatchEvent(new Event("t")); + target.dispatchEvent(new Event("t")); + expect(count).toBe(1); + }); + + it("removeEventListener stops future dispatches", function () { + var target = new EventTarget(); + var count = 0; + var handler = function () { count++; }; + target.addEventListener("t", handler); + target.dispatchEvent(new Event("t")); + target.removeEventListener("t", handler); + target.dispatchEvent(new Event("t")); + expect(count).toBe(1); + }); + + it("listeners run in registration order", function () { + var target = new EventTarget(); + var order = []; + target.addEventListener("t", function () { order.push(1); }); + target.addEventListener("t", function () { order.push(2); }); + target.addEventListener("t", function () { order.push(3); }); + target.dispatchEvent(new Event("t")); + expect(order).toEqual([1, 2, 3]); + }); + + it("stopImmediatePropagation stops remaining listeners", function () { + var target = new EventTarget(); + var order = []; + target.addEventListener("t", function (e) { order.push(1); e.stopImmediatePropagation(); }); + target.addEventListener("t", function () { order.push(2); }); + target.dispatchEvent(new Event("t")); + expect(order).toEqual([1]); + }); + + it("a throwing listener does not stop later listeners", function () { + var target = new EventTarget(); + var order = []; + target.addEventListener("t", function () { order.push(1); throw new Error("listener boom"); }); + target.addEventListener("t", function () { order.push(2); }); + target.dispatchEvent(new Event("t")); + expect(order).toEqual([1, 2]); + }); + }); + + it("reportError still fires listeners after globalThis.dispatchEvent is overwritten", function (done) { + var err = new Error("resilient"); + var received = null; + onGlobal("error", function (e) { + received = e; + e.preventDefault(); + }); + + var originalDispatch = globalThis.dispatchEvent; + globalThis.dispatchEvent = function () { return true; }; + try { + global.reportError(err); + } finally { + globalThis.dispatchEvent = originalDispatch; + } + + expect(received).not.toBeNull(); + expect(received.error).toBe(err); + afterQuietTurns(function () { + expect(uncaught.length).toBe(0); + done(); + }); + }); +}); diff --git a/test-app/app/src/main/assets/app/tests/testUnhandledRejections.js b/test-app/app/src/main/assets/app/tests/testUnhandledRejections.js new file mode 100644 index 000000000..ae8c5e6b2 --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/testUnhandledRejections.js @@ -0,0 +1,181 @@ +describe("unhandled promise rejections", function () { + // Unhandled rejections are tracked per-isolate and reported once per + // looper turn through the same uncaught-error machinery exposed via + // global.__onUncaughtError. Each test installs a temporary hook and + // restores the previous one in afterEach no matter what. Assertions are + // marker-based (matching this suite's own reasons) so stray rejections + // from other suites can never interfere. + var previousHook; + var reported; + var addedGlobalListeners; + + beforeEach(function () { + previousHook = global.__onUncaughtError; + reported = []; + global.__onUncaughtError = function (error) { + reported.push(error); + }; + addedGlobalListeners = []; + }); + + afterEach(function () { + global.__onUncaughtError = previousHook; + for (var i = 0; i < addedGlobalListeners.length; i++) { + var l = addedGlobalListeners[i]; + global.removeEventListener(l.type, l.handler); + } + addedGlobalListeners = []; + }); + + function onGlobal(type, handler) { + global.addEventListener(type, handler); + addedGlobalListeners.push({ type: type, handler: handler }); + } + + function reportedSeen(reason) { + return reported.indexOf(reason) !== -1; + } + + // The drain happens on a looper turn, so poll across a few turns until the + // predicate holds (or give up after a bounded number of turns). + function pollUntil(predicate, cb) { + var turns = 0; + (function poll() { + if (predicate() || turns >= 25) { + cb(); + return; + } + turns++; + setTimeout(poll, 10); + })(); + } + + // Wait a couple of looper turns to confirm something did NOT happen. + function afterQuietTurns(cb) { + setTimeout(function () { + setTimeout(cb, 20); + }, 20); + } + + it("reports an unhandled Promise.reject through __onUncaughtError", function (done) { + var reason = new Error("unhandled-promise-reject"); + Promise.reject(reason); + pollUntil(function () { return reportedSeen(reason); }, function () { + expect(reportedSeen(reason)).toBe(true); + done(); + }); + }); + + it("does not report when .catch is attached synchronously in the same turn", function (done) { + var reason = new Error("handled-same-turn"); + var p = Promise.reject(reason); + p.catch(function () {}); + afterQuietTurns(function () { + expect(reportedSeen(reason)).toBe(false); + done(); + }); + }); + + it("reports an uncaught throw from an async function", function (done) { + var reason = new Error("async-function-throw"); + (async () => { + throw reason; + })(); + pollUntil(function () { return reportedSeen(reason); }, function () { + expect(reportedSeen(reason)).toBe(true); + done(); + }); + }); + + it("reports an unhandled rejection thrown from a .then callback", function (done) { + var reason = new Error("then-callback-throw"); + Promise.resolve().then(function () { + throw reason; + }); + pollUntil(function () { return reportedSeen(reason); }, function () { + expect(reportedSeen(reason)).toBe(true); + done(); + }); + }); + + it("ignores a late .catch attached after the rejection was already reported", function (done) { + var reason = new Error("late-catch"); + var p = Promise.reject(reason); + pollUntil(function () { return reportedSeen(reason); }, function () { + expect(reportedSeen(reason)).toBe(true); + var countBefore = reported.length; + // Attaching a handler after the report was already delivered must + // not crash or report again. + p.catch(function () {}); + afterQuietTurns(function () { + expect(reported.length).toBe(countBefore); + done(); + }); + }); + }); + + it("unhandledrejection listener receives reason and promise; preventDefault suppresses the hook", function (done) { + var reason = new Error("rejected-with-listener"); + var received = null; + onGlobal("unhandledrejection", function (e) { + if (e.reason === reason) { + received = e; + e.preventDefault(); + } + }); + + Promise.reject(reason); + + pollUntil(function () { return received !== null; }, function () { + expect(received).not.toBeNull(); + expect(received instanceof PromiseRejectionEvent).toBe(true); + expect(received.type).toBe("unhandledrejection"); + expect(received.reason).toBe(reason); + expect(typeof received.promise.then).toBe("function"); + afterQuietTurns(function () { + expect(reportedSeen(reason)).toBe(false); + done(); + }); + }); + }); + + it("fires rejectionhandled when a handler is attached after the rejection was reported", function (done) { + var reason = new Error("late-handler"); + var rejectionHandled = null; + onGlobal("rejectionhandled", function (e) { + if (e.reason === reason) { + rejectionHandled = e; + } + }); + // Prevent the report so it does not hit the hook; the promise still + // counts as reported and becomes outstanding for rejectionhandled + // purposes. + var reportedPromise = null; + onGlobal("unhandledrejection", function (e) { + if (e.reason === reason) { + reportedPromise = e.promise; + e.preventDefault(); + } + }); + + var p = Promise.reject(reason); + + pollUntil(function () { return reportedPromise !== null; }, function () { + // Attach a late handler a couple turns after the report. + setTimeout(function () { + p.catch(function () {}); + pollUntil(function () { return rejectionHandled !== null; }, function () { + expect(rejectionHandled).not.toBeNull(); + expect(rejectionHandled instanceof PromiseRejectionEvent).toBe(true); + expect(rejectionHandled.type).toBe("rejectionhandled"); + expect(typeof rejectionHandled.promise.then).toBe("function"); + expect(rejectionHandled.promise).toBe(reportedPromise); + // The original rejection reason is retained past reporting + // and carried on the rejectionhandled event, per spec. + expect(rejectionHandled.reason).toBe(reason); + done(); + }); + }, 20); + }); + }); +}); diff --git a/test-app/app/src/main/java/com/tns/NativeScriptUncaughtExceptionHandler.java b/test-app/app/src/main/java/com/tns/NativeScriptUncaughtExceptionHandler.java index 75a96c2b0..7560b1d9d 100644 --- a/test-app/app/src/main/java/com/tns/NativeScriptUncaughtExceptionHandler.java +++ b/test-app/app/src/main/java/com/tns/NativeScriptUncaughtExceptionHandler.java @@ -22,6 +22,8 @@ public void uncaughtException(Thread thread, Throwable ex) { String stackTraceErrorMessage = Runtime.getStackTraceErrorMessage(ex); String errorMessage = String.format("%s\nStackTrace:\n%s", currentThreadMessage, stackTraceErrorMessage); + boolean handledByJs = false; + if (Runtime.isInitialized()) { try { if (Util.isDebuggableApp(context)) { @@ -31,7 +33,7 @@ public void uncaughtException(Thread thread, Throwable ex) { Runtime runtime = Runtime.getCurrentRuntime(); if (runtime != null) { - runtime.passUncaughtExceptionToJs(ex, ex.getMessage(), stackTraceErrorMessage, Runtime.getJSStackTrace(ex)); + handledByJs = runtime.passUncaughtExceptionToJs(ex, ex.getMessage(), stackTraceErrorMessage, Runtime.getJSStackTrace(ex)); } } catch (Throwable t) { if (Util.isDebuggableApp(context)) { @@ -44,6 +46,12 @@ public void uncaughtException(Thread thread, Throwable ex) { logger.write("Uncaught Exception Message=" + errorMessage); } + if (handledByJs) { + // A JS `error` event listener called preventDefault() - the + // exception is fully handled: no error activity, no crash. + return; + } + boolean res = false; if (Util.isDebuggableApp(context)) { diff --git a/test-app/runtime/CMakeLists.txt b/test-app/runtime/CMakeLists.txt index a8e428801..5aaff73c7 100644 --- a/test-app/runtime/CMakeLists.txt +++ b/test-app/runtime/CMakeLists.txt @@ -103,6 +103,8 @@ add_library( src/main/cpp/ConcurrentQueue.cpp src/main/cpp/Constants.cpp src/main/cpp/DirectBuffer.cpp + src/main/cpp/ErrorEvents.cpp + src/main/cpp/Events.cpp src/main/cpp/FieldAccessor.cpp src/main/cpp/File.cpp src/main/cpp/IsolateDisposer.cpp diff --git a/test-app/runtime/src/main/cpp/ErrorEvents.cpp b/test-app/runtime/src/main/cpp/ErrorEvents.cpp new file mode 100644 index 000000000..eb9cb19e5 --- /dev/null +++ b/test-app/runtime/src/main/cpp/ErrorEvents.cpp @@ -0,0 +1,244 @@ +#include "ErrorEvents.h" + +#include "ArgConverter.h" +#include "NativeScriptAssert.h" +#include "NativeScriptException.h" +#include "Runtime.h" + +using namespace std; +using namespace tns; +using namespace v8; + +/* + * Non-throwing runtime lookup, safe from V8 callbacks that may fire while a + * runtime is being torn down (Runtime::GetRuntime throws in that window). + */ +static Runtime* GetRuntimeOrNull(Isolate* isolate) { + return static_cast( + isolate->GetData((uint32_t) Runtime::IsolateData::RUNTIME)); +} + +/* + * Native function handed to the bootstrap IIFE as `nativeReportFatal(error, + * stackString)`. It runs the terminal tail (shim + log) WITHOUT re-dispatching + * an event: reportError and listener-thrown errors have already gone through + * JS dispatch, so dispatching again here would recurse. + */ +static void NativeReportFatalCallback(const FunctionCallbackInfo& info) { + auto isolate = info.GetIsolate(); + Local error = info.Length() > 0 ? info[0] + : Undefined(isolate).As(); + string stack; + if (info.Length() > 1 && info[1]->IsString()) { + stack = ArgConverter::ConvertToString(info[1].As()); + } + NativeScriptException::ReportFatalTail(isolate, error, stack); +} + +void ErrorEvents::Init(Local context) { + /* + * WHATWG error-events layer, layered on top of the generic event + * primitives installed by Events::Init and ported from the iOS runtime. + * Plain (module-free) script, strict inside the IIFE, ES5-ish so it never + * depends on other runtime extensions. The IIFE is invoked with two + * arguments - the internal EventTarget backing the global (so native + * dispatch survives app code overwriting globalThis.dispatchEvent) and + * the native nativeReportFatal(error, stack) function that runs the + * terminal tail - and returns three closures bound to that backing store. + * ErrorEvent/PromiseRejectionEvent subclass the Event captured off + * globalThis at init time, which runs before any user code. + */ + auto source = R"js( + (function (globalTarget, nativeReportFatal) { + "use strict"; + var g = globalThis; + var Event = g.Event; + + function ErrorEvent(type, opts) { + opts = opts || {}; + Event.call(this, type, opts); + this.message = opts.message !== undefined ? String(opts.message) : ""; + this.filename = opts.filename !== undefined ? String(opts.filename) : ""; + this.lineno = opts.lineno !== undefined ? (opts.lineno | 0) : 0; + this.colno = opts.colno !== undefined ? (opts.colno | 0) : 0; + this.error = opts.error !== undefined ? opts.error : null; + } + ErrorEvent.prototype = Object.create(Event.prototype); + ErrorEvent.prototype.constructor = ErrorEvent; + + function PromiseRejectionEvent(type, opts) { + opts = opts || {}; + Event.call(this, type, opts); + this.promise = opts.promise; + this.reason = opts.reason; + } + PromiseRejectionEvent.prototype = Object.create(Event.prototype); + PromiseRejectionEvent.prototype.constructor = PromiseRejectionEvent; + + // A listener that throws must not stop other listeners: route the thrown + // value to the native fatal tail instead of ever recursively dispatching + // another `error` event from inside dispatch. + globalTarget._installListenerErrorReporter(function (e) { + try { nativeReportFatal(e, (e && e.stack) || ""); } catch (ignored) {} + }); + + g.reportError = function (e) { + if (arguments.length === 0) { + throw new TypeError("Failed to execute 'reportError': 1 argument required, but only 0 present."); + } + var ev = new ErrorEvent("error", { + message: (e && e.message !== undefined && e.message !== null) ? String(e.message) : String(e), + error: e, + cancelable: true + }); + if (globalTarget.dispatchEvent(ev)) { + nativeReportFatal(e, (e && e.stack) || ""); + } + }; + + g.ErrorEvent = ErrorEvent; + g.PromiseRejectionEvent = PromiseRejectionEvent; + + // Closures called by C++. They never look up globalThis.dispatchEvent, + // so they keep working even if app code overwrites it. + function dispatchErrorEvent(error, message, stack) { + var ev = new ErrorEvent("error", { + message: message !== undefined && message !== null ? String(message) : "", + error: error, + cancelable: true + }); + globalTarget.dispatchEvent(ev); + return ev.defaultPrevented; + } + function dispatchUnhandledRejection(promise, reason) { + var ev = new PromiseRejectionEvent("unhandledrejection", { + promise: promise, + reason: reason, + cancelable: true + }); + globalTarget.dispatchEvent(ev); + return ev.defaultPrevented; + } + function dispatchRejectionHandled(promise, reason) { + var ev = new PromiseRejectionEvent("rejectionhandled", { + promise: promise, + reason: reason, + cancelable: false + }); + globalTarget.dispatchEvent(ev); + } + + return [dispatchErrorEvent, dispatchUnhandledRejection, dispatchRejectionHandled]; + }) + )js"; + + auto isolate = context->GetIsolate(); + auto runtime = GetRuntimeOrNull(isolate); + if (runtime == nullptr) { + throw NativeScriptException("ErrorEvents::Init: no runtime for isolate"); + } + if (runtime->GlobalEventTarget().IsEmpty()) { + throw NativeScriptException("ErrorEvents::Init: Events::Init must run first"); + } + Local globalTarget = runtime->GlobalEventTarget().Get(isolate); + + Local