From 144c2a2e031aa0984820101333e98ad5f4fc2290 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Thu, 23 Jul 2026 15:10:34 -0300 Subject: [PATCH] fix: report the rejection reason when ES module evaluation fails With top-level-await semantics, Module::Evaluate() never throws evaluation errors synchronously - it returns a promise that rejects, so the TryCatch around evaluation stays empty and the kRejected branch previously threw a synthetic message-only NativeScriptException ("Module evaluation promise rejected"). The actual error (e.g. a ReferenceError thrown while evaluating the module graph) was fetched via promise->Result() but never attached to the exception: the isolate->ThrowException(reason) call was swallowed by the still-active TryCatch on scope exit and nothing ever read it. Every outer layer (instantiate, require) then re-wrapped the placeholder, so users saw 'Module evaluation promise rejected' with no message or stack for the real failure. Re-throw the rejection reason on the isolate so the TryCatch captures it, then construct the NativeScriptException from the TryCatch - the same treatment synchronous evaluation errors get - so the original error message and JS stack trace propagate. Also log the error in debug builds, mirroring the synchronous evaluation failure path. --- NativeScript/runtime/ModuleInternal.mm | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/NativeScript/runtime/ModuleInternal.mm b/NativeScript/runtime/ModuleInternal.mm index ae56a9bf..6a8697ff 100644 --- a/NativeScript/runtime/ModuleInternal.mm +++ b/NativeScript/runtime/ModuleInternal.mm @@ -885,11 +885,21 @@ ScriptOrigin origin(isolate, urlString, 0, 0, false, -1, Local(), false, if (state == Promise::kRejected) { RemoveModuleFromRegistry(canonicalPath); logPhase("evaluate", "promise-rejected"); + if (!promiseTc.HasCaught()) { + // With top-level-await semantics, evaluation errors don't throw into + // the TryCatch — they surface only as the rejection reason of the + // evaluation promise. Re-throw the reason on the isolate so the + // TryCatch captures it and the exception below carries the original + // error message and stack trace instead of a synthetic placeholder. + isolate->ThrowException(promise->Result()); + } if (promiseTc.HasCaught()) { + if (RuntimeConfig.IsDebug) { + Log(@"Error evaluating ES module (rejected promise): %s", canonicalPath.c_str()); + tns::LogError(isolate, promiseTc); + } throw NativeScriptException(isolate, promiseTc, "Module evaluation promise rejected"); } else { - Local reason = promise->Result(); - isolate->ThrowException(reason); throw NativeScriptException(isolate, "Module evaluation promise rejected"); } }