Skip to content

fix: report the rejection reason when ES module evaluation fails - #410

Merged
NathanWalker merged 1 commit into
mainfrom
fix/esm-rejected-evaluation-error-reporting
Jul 23, 2026
Merged

fix: report the rejection reason when ES module evaluation fails#410
NathanWalker merged 1 commit into
mainfrom
fix/esm-rejected-evaluation-error-reporting

Conversation

@edusperoni

@edusperoni edusperoni commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

What is the current behavior?

When an ES module graph fails during evaluation (e.g. a ReferenceError thrown at module scope), the app crashes with an opaque, layered error that never mentions the actual failure:

Error: Error in require() call:
  Requested module: './'
  ...
Original error:
Cannot instantiate module /app/bundle.mjs
Module evaluation promise rejected
NativeScriptException: Module evaluation promise rejected
File: (<unknown>:1:231)

The real error message and JS stack trace are nowhere in the output.

Root cause: with top-level-await semantics, Module::Evaluate() never throws evaluation errors synchronously — it returns a promise that rejects. The kRejected branch in LoadESModule fetched the real error via promise->Result() but never attached it to the thrown exception:

Local<Value> reason = promise->Result();
isolate->ThrowException(reason);   // swallowed: promiseTc catches it and clears it on scope exit
throw NativeScriptException(isolate, "Module evaluation promise rejected");  // message-only constructor

The message-only NativeScriptException constructor fabricates a new Error from the fixed string, so every outer layer (instantiate of the importer, require() wrapper) re-wraps a placeholder while the original error is lost.

What is the new behavior?

The rejection reason is re-thrown on the isolate before constructing the exception, so the still-active TryCatch captures it and the NativeScriptException(isolate, TryCatch&, message) constructor extracts the original error message and JS stack trace — the same treatment synchronous evaluation errors already get. Debug builds also log the error, mirroring the synchronous evaluation-failure path. The message-only fallback is kept in case the TryCatch has nothing (defensive; should not happen).

Example: a webpack ESM bundle whose evaluation throws ReferenceError: NativeClass is not defined now reports that message and the frame in the module graph where it was thrown, instead of the generic placeholder above.

Verified with xcodebuild -scheme NativeScript -configuration Debug -sdk iphonesimulator build (BUILD SUCCEEDED).

Summary by CodeRabbit

  • Bug Fixes
    • Improved error handling for module loading failures involving asynchronous initialization.
    • Error messages and stack traces now preserve the original rejection reason, making top-level-await failures easier to diagnose.

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.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

LoadESModule now re-throws rejected top-level-await promise results through the V8 isolate before converting them into NativeScriptException, preserving the original rejection path during error handling.

Changes

ES module error handling

Layer / File(s) Summary
Top-level-await rejection propagation
NativeScript/runtime/ModuleInternal.mm
LoadESModule re-throws promise->Result() on rejected evaluation promises before applying promiseTc bookkeeping and NativeScriptException handling.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Poem

A bunny hopped through promises bright,
And tossed the true error into light.
V8 heard the reason clear,
No placeholder hiding near.
LoadESModule now thumps its feet—
Rejections land both sound and sweet!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: preserving and reporting the ES module evaluation rejection reason.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@NathanWalker
NathanWalker merged commit 164e752 into main Jul 23, 2026
7 of 8 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
NativeScript/runtime/ModuleInternal.mm (1)

888-904: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a regression test for rejected top-level-await errors.

Cover an ESM fixture that rejects with an Error, asserting that both direct loading and the require() wrapper preserve the original message and stack. The reported Debug build validates compilation, but not this runtime propagation contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@NativeScript/runtime/ModuleInternal.mm` around lines 888 - 904, Add a
regression test for the top-level-await rejection path around promiseTc and
NativeScriptException, using an ESM fixture that rejects with an Error. Assert
that both direct module loading and the require() wrapper preserve the original
error message and stack, covering the runtime propagation contract in Debug
builds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@NativeScript/runtime/ModuleInternal.mm`:
- Around line 888-904: Add a regression test for the top-level-await rejection
path around promiseTc and NativeScriptException, using an ESM fixture that
rejects with an Error. Assert that both direct module loading and the require()
wrapper preserve the original error message and stack, covering the runtime
propagation contract in Debug builds.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 59ad066d-e15f-45aa-afb9-c702acfd4ba2

📥 Commits

Reviewing files that changed from the base of the PR and between 485cdbb and 144c2a2.

📒 Files selected for processing (1)
  • NativeScript/runtime/ModuleInternal.mm

@edusperoni
edusperoni deleted the fix/esm-rejected-evaluation-error-reporting branch July 23, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants