Skip to content

fix: stop discarding errors in debug#375

Open
edusperoni wants to merge 4 commits into
mainfrom
fix/throw-errors
Open

fix: stop discarding errors in debug#375
edusperoni wants to merge 4 commits into
mainfrom
fix/throw-errors

Conversation

@edusperoni
Copy link
Copy Markdown
Collaborator

@edusperoni edusperoni commented May 17, 2026

turns out I missed a few errors in my other PR.

Yes the app should crash in debug. Right now it crashes in release mode but continues fine in debug (?????????????)

Summary by CodeRabbit

  • Bug Fixes

    • Removed a debug-only mechanism that could keep the app running after JavaScript errors; unrecoverable errors now terminate as expected.
    • Worker error propagation tightened so uncaught worker errors are delivered reliably to the main runtime.
  • Refactor

    • Simplified module-loading and runtime error flows to surface exceptions immediately rather than attempting debug-only graceful continuation.
    • Improved module resolution and diagnostic logging for clearer failure messages.

Review Change Stack

@NathanWalker
Copy link
Copy Markdown
Contributor

@coderabbitai review
Also consider whether this adversely affects the nativescript.config.ts > showErrorDisplay: true option.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

🧩 Analysis chain

Let me gather the PR details and search for the showErrorDisplay usage.


🏁 Script executed:

#!/bin/bash
# Get PR diff
gh pr view 375 --repo NativeScript/ios --json files,body,title
echo "---DIFF---"
gh pr diff 375 --repo NativeScript/ios

Length of output: 139107


🏁 Script executed:

#!/bin/bash
# Search for showErrorDisplay references
rg -n "showErrorDisplay" --type-add 'ts:*.ts' --type-add 'mm:*.mm' -C5

# Also search for error display related code
rg -n "ErrorDisplay\|showError\|TNSExceptionHandler\|handleException\|discardErrors\|debugMode\|isDebug" -C5

Length of output: 5130


@NathanWalker — I've reviewed the diff in detail. Here is the full analysis:


Review Summary

✅ Core fix is correct

The removal of jsErrorOccurred, the while(true) infinite run loop, all the return true ("lie to iOS") patterns, and the ShowErrorModal-then-return paths in OnUncaughtError / ReThrowToV8 is the right direction. Errors should propagate and crash the app in debug just as they do in release.


⚠️ showErrorDisplay: true is adversely affected for uncaught JS exceptions

There are two separate pathways for the error display feature, and they are now in an inconsistent state:

Path 1 — console.error with stack trace → still works ✅
(Console.cpp:111) — this path still calls NativeScriptException::SubmitConsoleErrorPayload when Runtime::showErrorDisplay() is true. Not touched by this PR.

Path 2 — Uncaught JS exceptions → now broken ❌
Previously in OnUncaughtError (debug branch) and in ReThrowToV8 (critical exception branch), ShowErrorModal(...) was called before returning. Both of those call sites have been removed. RenderErrorModalUI still exists (and still guards on Runtime::showErrorDisplay()), but it is no longer invoked for uncaught exception scenarios.

The result: if showErrorDisplay: true is set, users will no longer see the branded error overlay when an uncaught JS exception occurs — the app will just crash with only a log output.

Suggested approach: rather than removing the ShowErrorModal call entirely from OnUncaughtError, consider calling it before the crash (not instead of it):

// In OnUncaughtError, after logging:
if (RuntimeConfig.IsDebug && Runtime::showErrorDisplay()) {
    ShowErrorModal(isolate, errorTitle, errorMessage, stackTrace);
    // Small synchronous delay to allow modal to render before the crash
}
// Then let the crash proceed normally (don't return)
dispatch_async(dispatch_get_main_queue(), ^(void) {
    `@throw` [NSException exceptionWithName:name reason:reasonStr userInfo:...];
});

⚠️ Release mode crash path in OnUncaughtError also removed

The old OnUncaughtError contained (under if (!RuntimeConfig.IsDebug)) a dispatch_async that threw an NSException to terminate the app in release. That block is gone in this PR. The new code only logs.

If the crash in release mode is now relying entirely on V8's own unhandled exception machinery propagating up the call stack (rather than the explicit NSException throw), that should be verified — especially for async/promise rejection scenarios where the V8 callback may return cleanly to its caller.


Minor nits (non-blocking)

The large volume of formatting-only changes (whitespace, line wrapping) makes the diff harder to review. Consider splitting those into a separate commit for clarity.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Warning

Rate limit exceeded

@edusperoni has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 55 minutes and 51 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 758985b7-71d0-4afd-afad-2602d910938a

📥 Commits

Reviewing files that changed from the base of the PR and between dc46a79 and a9b1610.

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

Walkthrough

This PR removes the global jsErrorOccurred flag and refactors error handling throughout the NativeScript iOS runtime to propagate exceptions directly via V8 instead of silently continuing in debug mode. The changes span module loading, resolution, dynamic imports, worker threads, and app-level fatal error logging.

Changes

Error Propagation and Exception Handling

Layer / File(s) Summary
Global jsErrorOccurred flag removal and app-level error handling
NativeScript/NativeScript.mm, NativeScript/runtime/Runtime.mm, NativeScript/runtime/NativeScriptException.mm
Removes the global jsErrorOccurred flag from declarations, deletes the debug-only infinite CFRunLoopRunInMode loop that kept the app alive on JS errors, and simplifies fatal error handling to log directly without debug modal continuation.
Module loading error propagation
NativeScript/runtime/ModuleInternal.mm
RunModule, CommonJS require() and ES module fast paths now rethrow NativeScriptException into V8 on failure rather than using debug-only graceful continuation. GetRequireFunction now throws on factory failure. LoadModule, LoadScript, and LoadClassicScript log and throw instead of returning empty/dummy values.
Module compilation and registry refactoring
NativeScript/runtime/ModuleInternalCallbacks.mm (compilation, registry, tracking)
Adds compilation helpers with richer debug diagnostics (hash/snippet/classification), tightens registry removal and fallback clearing, and introduces explicit waiter maps for module and HTTP dynamic-import coalescing.
Module resolution normalization and HTTP handling
NativeScript/runtime/ModuleInternalCallbacks.mm (ResolveModuleCallback)
Normalizes malformed HTTP schemes and @-prefixed paths, refactors absolute /app/... candidate resolution, compiles/registers HTTP-static candidates during resolution, implements embedded-HTTP rerouting for collapsed paths, and adds a Documents/_ns_hmr mirror fallback.
Dynamic import and TLA handling
NativeScript/runtime/ModuleInternalCallbacks.mm (ImportModuleDynamicallyCallback)
Improves @-spec validation with JS stack capture, normalizes dynamic specifiers, reworks HTTP dynamic import concurrency with then/resolve/reject handlers that drain waiter queues, enhances instantiate/evaluate error diagnostics, and explicitly rejects when default becomes a TDZ post-evaluation.
Worker thread error handling alignment
NativeScript/runtime/Worker.mm
Removes the debug-mode jsErrorOccurred synthetic payload path. Worker threads now rely solely on TryCatch after RunModule, dispatching errors to main via existing async path (comments added to clarify timing relative to worker.onerror).
Runtime formatting and comment clarifications
NativeScript/runtime/Runtime.mm
Reorders include directives, clarifies inline comments about app-config caching nil/NSNull, and reformats queueMicrotask lambda wrapping without behavioral changes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 No more silent sleeps when errors appear,
The flag is gone—exceptions ring clear.
From NativeScript's core to workers in thread,
Errors now flow, no debug lies are spread.
Module by module, the truth finds its way. 🌟

🚥 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 'fix: stop discarding errors in debug' directly and accurately summarizes the main change across all modified files—removing error-discarding mechanisms that previously continued execution in debug mode.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
NativeScript/runtime/ModuleInternal.mm (1)

166-179: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

RunModule still treats some .mjs failures as success in debug.

LoadESModule() still has debug-only branches that return an empty handle on compile/link/evaluate failure, and this fast path returns true unconditionally. That means those .mjs errors still continue execution instead of failing the app.

🤖 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 166 - 179, The ES module
fast-path in RunModule treats an empty module handle as success; update the
IsESModule branch (where TryCatch tc and moduleNamespace =
ModuleInternal::LoadESModule(isolate, path) are used) to check whether
moduleNamespace is empty or tc.HasCaught() after the call, and if so log as
appropriate and return false (or rethrow the V8 exception via ex.ReThrowToV8 /
propagate the TryCatch) instead of unconditionally returning true; ensure
LoadESModule failure paths that return an empty Local<Value> do not get treated
as success.
🤖 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.

Inline comments:
In `@NativeScript/runtime/ModuleInternal.mm`:
- Around line 916-920: The current branch uses isolate->ThrowException(reason)
then constructs NativeScriptException(isolate, promiseTc, "Module evaluation
promise rejected"), but promiseTc is not populated by ThrowException so the
exception context is lost; change the throw to use the message-only
constructor—throw NativeScriptException(isolate, "Module evaluation promise
rejected")—so NativeScriptException and downstream helpers like
GetSmartStackTrace/GetFullMessage receive a proper message-only construction
instead of an empty TryCatch; update the throw site that follows the
promiseTc/HasCaught logic around promise->Result() accordingly.

In `@NativeScript/runtime/ModuleInternalCallbacks.mm`:
- Around line 2421-2433: The code rejects and returns inside the first
maybeModule.IsEmpty() check (both resolveTc.HasCaught() and else branches),
making the later fallback that calls __nsHmrRequestModule() unreachable; fix by
restructuring so that when maybeModule.IsEmpty() and no exception was thrown you
do not immediately return but instead attempt the HMR retry: remove the early
return in the "No exception thrown (debug path)" branch (the branch building msg
with normalizedSpec and calling resolver->Reject) and instead call
__nsHmrRequestModule(context, normalizedSpec, resolver) (or the existing retry
path used later) so the dynamic import can be retried via __nsHmrRequestModule()
before finally rejecting via resolver->Reject; ensure resolveTc.HasCaught()
still rejects immediately with resolveTc.Exception() via resolver->Reject and
return as before.

In `@NativeScript/runtime/NativeScriptException.mm`:
- Around line 157-162: The uncaught-exception branch only logs now and must
still render the debug error overlay; before or immediately after detecting
!isDiscarded in the handler in NativeScriptException.mm, invoke the existing
error-display path (call OnUncaughtError(...) or directly call
ShowErrorModal/ShowErrorDisplay with showErrorDisplay: true) using the same
fullMessage/stackTrace so the debug modal is shown for uncaught JS exceptions,
then allow the exception to continue propagating/being logged as currently done
(references: isDiscarded, fullMessage, stackTrace, OnUncaughtError,
ShowErrorModal, showErrorDisplay).

---

Outside diff comments:
In `@NativeScript/runtime/ModuleInternal.mm`:
- Around line 166-179: The ES module fast-path in RunModule treats an empty
module handle as success; update the IsESModule branch (where TryCatch tc and
moduleNamespace = ModuleInternal::LoadESModule(isolate, path) are used) to check
whether moduleNamespace is empty or tc.HasCaught() after the call, and if so log
as appropriate and return false (or rethrow the V8 exception via ex.ReThrowToV8
/ propagate the TryCatch) instead of unconditionally returning true; ensure
LoadESModule failure paths that return an empty Local<Value> do not get treated
as success.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2fc1168c-e6d8-456c-82bd-18887238b219

📥 Commits

Reviewing files that changed from the base of the PR and between 90ac16c and f7133de.

📒 Files selected for processing (6)
  • NativeScript/NativeScript.mm
  • NativeScript/runtime/ModuleInternal.mm
  • NativeScript/runtime/ModuleInternalCallbacks.mm
  • NativeScript/runtime/NativeScriptException.mm
  • NativeScript/runtime/Runtime.mm
  • NativeScript/runtime/Worker.mm

Comment thread NativeScript/runtime/ModuleInternal.mm Outdated
Comment on lines +2421 to +2433
if (maybeModule.IsEmpty()) {
if (resolveTc.HasCaught()) {
// Reject the promise with the thrown exception so callers don't hang
resolver->Reject(context, resolveTc.Exception()).FromMaybe(false);
return scope.Escape(resolver->GetPromise());
} else {
// No exception thrown (debug path); reject with a helpful error
std::string msg = "Module resolution failed for dynamic import: ";
msg += normalizedSpec.empty() ? "<empty>" : normalizedSpec;
resolver->Reject(context, v8::Exception::Error(tns::ToV8String(isolate, msg.c_str())))
.FromMaybe(false);
return scope.Escape(resolver->GetPromise());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The __nsHmrRequestModule() fallback is unreachable.

The first maybeModule.IsEmpty() block already rejects and returns in both branches, so the later maybeModule.IsEmpty() block never gets a chance to call __nsHmrRequestModule(). Dynamic imports that should retry through the HMR bridge will now fail immediately.

Also applies to: 2436-2452

🤖 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/ModuleInternalCallbacks.mm` around lines 2421 - 2433,
The code rejects and returns inside the first maybeModule.IsEmpty() check (both
resolveTc.HasCaught() and else branches), making the later fallback that calls
__nsHmrRequestModule() unreachable; fix by restructuring so that when
maybeModule.IsEmpty() and no exception was thrown you do not immediately return
but instead attempt the HMR retry: remove the early return in the "No exception
thrown (debug path)" branch (the branch building msg with normalizedSpec and
calling resolver->Reject) and instead call __nsHmrRequestModule(context,
normalizedSpec, resolver) (or the existing retry path used later) so the dynamic
import can be retried via __nsHmrRequestModule() before finally rejecting via
resolver->Reject; ensure resolveTc.HasCaught() still rejects immediately with
resolveTc.Exception() via resolver->Reject and return as before.

Comment on lines 157 to 162
if (!isDiscarded) {
NSString* reasonStr = [NSString stringWithUTF8String:fullMessage.c_str()];
if (reasonStr == nil) {
reasonStr = @"(invalid UTF-8 message from JS)";
}

NSString* name = @"NativeScriptUncaughtJSException";

// In debug mode, show error modal instead of crashing
if (RuntimeConfig.IsDebug) {
// Mark that a JavaScript error occurred
jsErrorOccurred = true;
Log(@"***** JavaScript exception occurred "
@"in debug mode *****\n");
Log(@"%s", fullMessage.c_str());
Log(@"***** Fatal JavaScript exception *****\n");
Log(@"%s", fullMessage.c_str());
if (!stackTrace.empty()) {
Log(@"%s", stackTrace.c_str());
// Log(@"🎨 CALLING ShowErrorModal for OnUncaughtError - should display branded modal");

// Show the error modal with same message as terminal
std::string errorTitle = "Uncaught JavaScript Exception";

// Extract just the error type/message (first line) for cleaner display
std::string errorMessage = "JavaScript error occurred";
if (reasonStr) {
std::string fullMsg = [reasonStr UTF8String];
size_t firstNewline = fullMsg.find('\n');
if (firstNewline != std::string::npos) {
errorMessage = fullMsg.substr(0, firstNewline);
} else {
errorMessage = fullMsg;
}
}

Log(@"***** End stack trace - Fix error to continue *****\n");

ShowErrorModal(isolate, errorTitle, errorMessage, stackTrace);

// Don't crash in debug mode - just return
return;
}

// In release mode, crash as before - BUT NEVER IN DEBUG MODE
if (!RuntimeConfig.IsDebug) {
// we throw the exception on main thread so all meta-data is captured
dispatch_async(dispatch_get_main_queue(), ^(void) {
NSException* objcException =
[NSException exceptionWithName:name
reason:reasonStr
userInfo:@{@"sender" : @"onUncaughtError"}];

Log(@"***** Fatal JavaScript exception - application has been terminated. *****\n");
Log(@"%@", objcException);
@throw objcException;
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep showErrorDisplay active for uncaught exceptions.

This branch now only logs. Since OnUncaughtError no longer invokes ShowErrorModal(), showErrorDisplay: true stops showing the debug overlay for uncaught JS exceptions on the exact path this PR is changing. If debug should still crash, render the modal here first and then let the exception continue propagating.

🤖 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/NativeScriptException.mm` around lines 157 - 162, The
uncaught-exception branch only logs now and must still render the debug error
overlay; before or immediately after detecting !isDiscarded in the handler in
NativeScriptException.mm, invoke the existing error-display path (call
OnUncaughtError(...) or directly call ShowErrorModal/ShowErrorDisplay with
showErrorDisplay: true) using the same fullMessage/stackTrace so the debug modal
is shown for uncaught JS exceptions, then allow the exception to continue
propagating/being logged as currently done (references: isDiscarded,
fullMessage, stackTrace, OnUncaughtError, ShowErrorModal, showErrorDisplay).

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