GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — feat: web-compliant error handling by edusperoni · Pull Request #1985 · NativeScript/android Conversation
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe runtime adds WHATWG-style event primitives, native error-event dispatch, unhandled promise rejection tracking, configurable uncaught-error handling, Java exception escaping with JavaScript stack preservation, and corresponding tests and documentation. ChangesRuntime Error Handling
Estimated code review effort: 4 (Complex) | ~75 minutes Sequence Diagram(s)sequenceDiagram
participant Java
participant Runtime
participant ErrorEvents
participant EventTarget
Java->>Runtime: pass uncaught exception
Runtime->>ErrorEvents: dispatch error event
ErrorEvents->>EventTarget: notify error listeners
EventTarget-->>ErrorEvents: return preventDefault result
ErrorEvents-->>Runtime: return handled status
Runtime-->>Java: return handling result
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
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. Comment |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
test-app/app/src/main/assets/app/tests/testErrorEvents.js (1)
38-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract shared
onGlobal/afterQuietTurns/pollUntiltest helpers.Both new spec files reimplement the same global-listener bookkeeping and quiet-turn waiting logic, with
afterQuietTurnsusing different (25ms vs 20ms) delays in each file. Moving these into a shared test-utility module would remove duplication and prevent timing drift between suites.
test-app/app/src/main/assets/app/tests/testErrorEvents.js#L38-L48: extractonGlobalandafterQuietTurnsinto a shared helper module imported by both spec files.test-app/app/src/main/assets/app/tests/testUnhandledRejections.js#L30-L58: replace the localonGlobal/afterQuietTurns(and optionallypollUntil) with the shared helper module, reconciling the differing timing constants.🤖 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 `@test-app/app/src/main/assets/app/tests/testErrorEvents.js` around lines 38 - 48, Extract the duplicated onGlobal and afterQuietTurns helpers into a shared test utility module, using one reconciled quiet-turn delay for both suites. In test-app/app/src/main/assets/app/tests/testErrorEvents.js:38-48, replace the local implementations with imports from that module; in test-app/app/src/main/assets/app/tests/testUnhandledRejections.js:30-58, likewise remove the local onGlobal/afterQuietTurns implementations and optionally reuse pollUntil through the shared module. Update both suites to use the shared helper consistently.
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 `@test-app/app/src/main/assets/app/tests/testErrorEvents.js`:
- Around line 38-48: Extract the duplicated onGlobal and afterQuietTurns helpers
into a shared test utility module, using one reconciled quiet-turn delay for
both suites. In test-app/app/src/main/assets/app/tests/testErrorEvents.js:38-48,
replace the local implementations with imports from that module; in
test-app/app/src/main/assets/app/tests/testUnhandledRejections.js:30-58,
likewise remove the local onGlobal/afterQuietTurns implementations and
optionally reuse pollUntil through the shared module. Update both suites to use
the shared helper consistently.
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 51a14569-4f23-4a62-8ac5-f6f380f301bd
test-app/app/src/main/assets/app/mainpage.jstest-app/app/src/main/assets/app/tests/testErrorEvents.jstest-app/app/src/main/assets/app/tests/testUnhandledRejections.jstest-app/app/src/main/java/com/tns/NativeScriptUncaughtExceptionHandler.javatest-app/runtime/CMakeLists.txttest-app/runtime/src/main/cpp/ErrorEvents.cpptest-app/runtime/src/main/cpp/ErrorEvents.htest-app/runtime/src/main/cpp/Events.cpptest-app/runtime/src/main/cpp/Events.htest-app/runtime/src/main/cpp/NativeScriptException.cpptest-app/runtime/src/main/cpp/NativeScriptException.htest-app/runtime/src/main/cpp/Runtime.cpptest-app/runtime/src/main/cpp/Runtime.htest-app/runtime/src/main/cpp/com_tns_Runtime.cpptest-app/runtime/src/main/java/com/tns/Runtime.java
Sorry, something went wrong.
There was an error while loading. Please reload this page.
…tive callers) Ports the identity-preservation half of iOS's interop.escapeException (NativeScript/ios#409). The escape direction itself is already Android's default - a JS throw in a native-invoked override surfaces to the Java caller as a real com.tns.NativeScriptException - but a rethrown *Java* exception lost its identity: ReThrowToJava wrapped it, so the caller's catch of the concrete type (e.g. IOException) never matched. Now, mirroring iOS: - New `interop` global (per isolate, workers included) with interop.escapeException(x): returns a fresh JS Error branded via a private symbol whose payload carries the original Java throwable when x is/carries one (nativeException), or {name, message, stack} synthesis info otherwise. Idempotent; TypeError with no arguments. - At the JS->Java boundary, a branded error carrying a Java throwable is rethrown UNWRAPPED (env.Throw of the original object), so a native `catch (IOException e)` above the caller matches and Throwable identity is preserved. JNI does not enforce checked-exception declarations, so any Throwable propagates. - Branded errors bypass discardUncaughtJsExceptions (which only handles com.tns.NativeScriptException) - an explicit forward request, matching iOS semantics where branded escapes ignore the discard flag. - Unbranded throws keep today's semantics exactly (wrapper with the original as cause); branded plain JS errors keep the default NativeScriptException escape shape. Testing: 7 new specs (brand semantics + boundary round-trips through a new EscapeExceptionTest fixture, asserting the caller catches the original java.io.IOException by identity, and that unbranded/plain behavior is unchanged). Full suite green on an API 35 arm64 emulator.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
test-app/runtime/src/main/cpp/NativeScriptException.cpp (1)
472-560: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
draining_can get stucktrueforever if a non-NativeScriptExceptionescapesDrain().Both loops only catch
NativeScriptException&, anddraining_ = false(line 559) plusPruneReportedOutstanding()(line 557) only run if nothing throws past the catch blocks. IfErrorEvents::DispatchRejectionHandled/DispatchUnhandledRejection,GetStackTraceOfValue,ToDetailString,GiveWorkerOnErrorAChance,PassUncaughtExceptionFromWorkerToParent, orPruneReportedOutstanding()throws anything other thanNativeScriptException(e.g. a JNI/std exception), it propagates out ofDrain()uncaught, leavingdraining_permanentlytrue. SinceDrain()early-returns wheneverdraining_is true (line 474-476), this silently and permanently disables all futureunhandledrejection/rejectionhandleddispatch for the isolate.Use an RAII guard so
draining_is always reset regardless of how the function exits, and widen the catches so no exception type can escape this loop:🛡️ Proposed fix
void PromiseRejectionTracker::Drain() { drainScheduled_ = false; if (draining_) { return; } draining_ = true; + struct DrainGuard { + bool& flag; + ~DrainGuard() { flag = false; } + } drainGuard{draining_}; auto isolate = runtime_->GetIsolate(); auto context = isolate->GetCurrentContext(); ... } catch (NativeScriptException& ex) { DEBUG_WRITE_FORCE( "PromiseRejectionTracker: exception while firing rejectionhandled: %s", ex.GetErrorMessage().c_str()); + } catch (...) { + DEBUG_WRITE_FORCE( + "PromiseRejectionTracker: unknown exception while firing rejectionhandled"); } ... } catch (NativeScriptException& ex) { DEBUG_WRITE_FORCE( "PromiseRejectionTracker: exception while reporting rejection: %s", ex.GetErrorMessage().c_str()); + } catch (...) { + DEBUG_WRITE_FORCE( + "PromiseRejectionTracker: unknown exception while reporting rejection"); } } PruneReportedOutstanding(); - - draining_ = false; }🤖 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 `@test-app/runtime/src/main/cpp/NativeScriptException.cpp` around lines 472 - 560, Update PromiseRejectionTracker::Drain to use an RAII guard that resets draining_ on every exit, including exceptions from PruneReportedOutstanding. Widen both loop-level catches, including the rejectionhandled dispatch and unhandled rejection reporting paths, so non-NativeScriptException failures cannot escape Drain; preserve the existing diagnostic logging and dispatch behavior.
test-app/runtime/src/main/cpp/Interop.cpp (1)
34-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReuse
NativeScriptException::THROWABLE_CLASSfor the throwableIsAssignableFromcheck.
GetWrappedJavaThrowableandInterop::ExtractEscapedJavaExceptionboth callenv.FindClass("java/lang/Throwable"), whileTryGetJavaThrowableObjectalready uses the cachedNativeScriptException::THROWABLE_CLASSfor the same check. Reusing the cached reference avoids duplicate JNI lookups and keeps a single throwable class reference in use.Also applies to: 179-216
🤖 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 `@test-app/runtime/src/main/cpp/Interop.cpp` around lines 34 - 65, Update GetWrappedJavaThrowable and Interop::ExtractEscapedJavaException to use the cached NativeScriptException::THROWABLE_CLASS reference for their IsAssignableFrom checks instead of calling env.FindClass("java/lang/Throwable"). Preserve the existing throwable detection behavior and apply the same reuse consistently in both methods.
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@test-app/runtime/src/main/cpp/NativeScriptException.cpp`:
- Around line 472-560: Update PromiseRejectionTracker::Drain to use an RAII
guard that resets draining_ on every exit, including exceptions from
PruneReportedOutstanding. Widen both loop-level catches, including the
rejectionhandled dispatch and unhandled rejection reporting paths, so
non-NativeScriptException failures cannot escape Drain; preserve the existing
diagnostic logging and dispatch behavior.
---
Nitpick comments:
In `@test-app/runtime/src/main/cpp/Interop.cpp`:
- Around line 34-65: Update GetWrappedJavaThrowable and
Interop::ExtractEscapedJavaException to use the cached
NativeScriptException::THROWABLE_CLASS reference for their IsAssignableFrom
checks instead of calling env.FindClass("java/lang/Throwable"). Preserve the
existing throwable detection behavior and apply the same reuse consistently in
both methods.
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 106e6980-5592-4b35-92f3-1a57c6bc9883
test-app/app/src/main/assets/app/mainpage.jstest-app/app/src/main/assets/app/tests/testEscapeException.jstest-app/app/src/main/java/com/tns/tests/EscapeExceptionTest.javatest-app/runtime/CMakeLists.txttest-app/runtime/src/main/cpp/Interop.cpptest-app/runtime/src/main/cpp/Interop.htest-app/runtime/src/main/cpp/NativeScriptException.cpptest-app/runtime/src/main/cpp/Runtime.cpp
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Companion to the iOS runtime's JS-stack DX work on NativeScript/ios#409, using the JVM primitive iOS doesn't have: Throwable.addSuppressed. - New com.tns.JavaScriptStackTrace: a never-thrown Throwable carrying a JS stack, with StackTraceElements synthesized from the V8 frames (its own Java capture is suppressed). Attached generically via attach() so future exception paths can adopt it; SDK integrations read the raw stacks through getJavaScriptStack()/getEscapeSiteStack(). - Original-throwable escapes: the boundary attaches the carrier as a suppressed exception before rethrowing, so the JS journey renders automatically in printStackTrace(), logcat fatal logs and crash reporters ("Suppressed: com.tns.JavaScriptStackTrace: ...") while the original object's identity, class, stack and cause chain stay untouched. Idempotent: the same throwable escaping twice through nested overrides gets one carrier. - Escape-site capture: escapeException() now records the stack of its own call site (the branded Error's natural V8 stack, grabbed before the origin stack overwrites it) alongside the origin stack. For non-Error values it is the only stack available and was previously lost. - Synthesized escapes: the com.tns.NativeScriptException thrown for a branded error with no underlying Java throwable gets its stack trace replaced with the synthesized JS frames, so crash reporters group these by where they actually happened in JS instead of bucketing everything under the identical JNI boundary machinery. Testing: 3 new specs (suppressed carrier contents + escape-site accessor, duplicate-carrier guard across nested escapes, non-Error escape-site fallback) plus JS-frame assertions on the synthesized path. Full suite green on an API 35 arm64 emulator.
Documents the web-compliant error model: global error/unhandledrejection/ rejectionhandled events and reportError, Java exception round-tripping (error.nativeException), interop.escapeException and the com.tns.JavaScriptStackTrace carrier, configuration flags with the terminal-path decision table, and crash-reporter integration on both the JS and Java sides. Adds a docs/ index and links it from the README, mirroring the docs added on the iOS PR (NativeScript/ios#409).
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
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 `@docs/error-handling.md`:
- Line 43: Update the cancelable `error` and `unhandledrejection` documentation
to state that `preventDefault()` suppresses downstream hooks, error activity,
and process crashes, but does not necessarily suppress native pre-dispatch
logging such as `System.err` or logger output.
- Around line 141-143: Update the Java example around the suppressed-exception
loop to declare and initialize caught before calling caught.getSuppressed(), or
move the loop into the preceding catch block where caught is already in scope,
ensuring the copied example compiles.
- Around line 129-136: Update the fenced code block containing the Java
stack-trace example to declare the text language identifier, changing the
opening fence to use text while preserving the stack-trace content unchanged.
Fix all unresolved CodeRabbit comments on this PR:
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 3945e59d-d2cd-4ec0-b4d8-e907a05f318c
README.mddocs/README.mddocs/error-handling.md
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
| for (Throwable suppressed : caught.getSuppressed()) { | ||
| if (suppressed instanceof com.tns.JavaScriptStackTrace) { | ||
| com.tns.JavaScriptStackTrace jsTrace = (com.tns.JavaScriptStackTrace) suppressed; |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Define caught before using it.
The Java example calls caught.getSuppressed() without declaring caught, so copied code does not compile. Use a declared exception variable or place the loop inside the preceding catch block.
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/error-handling.md` around lines 141 - 143, Update the Java example
around the suppressed-exception loop to declare and initialize caught before
calling caught.getSuppressed(), or move the loop into the preceding catch block
where caught is already in scope, ensuring the copied example compiles.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Adopts the web's model as the default: an erroring page doesn't crash the browser, and an uncaught JS error no longer crashes the app. This is the Android half of the unified NativeScript 9.1 error-handling spec. Containment: - An uncaught throw in a NATIVE-initiated callback (the OS invoking an overridden method or interface implementation, a posted Runnable, a timer, __runOnMainThread, a frame callback) is contained at the boundary: reported through the WHATWG pipeline (cancelable `error` event -> __onUncaughtError -> logcat) and the native caller resumes with the return type's default value. preventDefault() suppresses the report entirely. - A JS-INITIATED chain (JS -> Java -> JS callback throws) keeps propagating to the outer JS catch with the original JS error object - correct JavaScript semantics, tracked via a per-isolate JS->Java call depth around CallJavaMethod/RegisterInstance. Containment applies only at an outermost native-initiated boundary. - Contained primitive-returning overrides yield the type's default (dispatchCallJSMethodNative substitutes it before unboxing), fixing the NPE the old discard path produced. - Branded interop.escapeException throws are never contained; module/ script evaluation (bootstrap) is not contained. uncaughtErrorPolicy (app package.json, root level): - "report" (default): report and continue, never crash. - "throw": unprevented errors are thrown to the native layer as real Java exceptions - com.tns.NativeScriptException with the JS frames as its stack trace - restoring the pre-9.1 behavior (typically a crash, via the thread's uncaught-exception path, which performs the single report). Unhandled rejections are thrown from a clean Java frame posted to the runtime's looper. Named for the mechanism, not a guaranteed crash: native catch-alls above the boundary can still intercept. discardUncaughtJsExceptions is deprecated (logcat warning): `true` keeps its legacy quiet routing (__onDiscardedError, no log) on top of the new default; `false` - which used to mean "crash" - is ignored. Branded escapes are now exempt from the discard handling (escapedFromJs marker). Testing: 5 new specs (containment + app liveness with event/hook identity, preventDefault suppression, timer containment, primitive default instead of NPE, JS-initiated chain identity) plus the direct-throw escape specs; "throw" policy is a documented manual smoke. Full suite green on an API 35 arm64 emulator - every pre-existing suite passes unchanged thanks to the JS-initiated-chain rule.
… parity) Parity round with NativeScript/ios#409's claim-slot commit, closing the review's cross-platform divergences: - Single report per failure under uncaughtErrorPolicy: "throw": the full report (cancelable event -> hook -> log) now runs at the decision point BEFORE the native throw, for both the sync boundary and the rejection drain, and the thrown com.tns.NativeScriptException is marked reportedToJs (isolate-private brand carried to a Java field by ReThrowToJava; set directly for the rejection looper throw) so NativeScriptUncaughtExceptionHandler skips the post-mortem re-report. Previously a "throw"-policy rejection fired unhandledrejection AND a second post-mortem error event. The public NativeScriptException.isReportedToJs() lets custom Thread.UncaughtExceptionHandler implementations honor the same contract. - preventDefault() now beats the "throw" policy at the sync boundary too: the event dispatches before the propagation decision, so a prevented error is fully contained with the app alive (iOS behavior). The deprecated discard flag disables the throw, matching iOS. - error.stackTrace / reason.stackTrace (the combined legacy stack, a NativeScript extension) are populated BEFORE the error / unhandledrejection events dispatch - including the worker branch of the rejection drain - so listeners see the same shape the hooks do; pinned by new spec assertions mirroring the iOS ones. - Docs: unified "throw" sequencing and the cross-platform catchability note (Android sync at the method boundary; iOS sync only at own-frame-reporting boundaries, deferred for block/method callbacks and loop-originated errors; escapeException is the portable per-call interception tool), plus the Android-only native-crash event asymmetry and the interop feature-detection note. Full suite green on an API 35 arm64 emulator: 604 specs, 0 failures.
Separates native-layer deaths from the WHATWG `error` event, completing the invariant that error/unhandledrejection only fire while the failure is still containable (the app fully alive, preventDefault() honest). - New cancelable `nativeuncaughterror` ErrorEvent, dispatched synchronously from the uncaught-exception handler for everything that reaches it un-marked: pure-native Java crashes, uncaught interop.escapeException forwards, bootstrap failures. The handler classifies nothing - the simple rule IS the spec. e.error carries the wrapped original Throwable via nativeException. preventDefault() is best-effort: it skips the error activity and the default (killing) handler - meaningful for background-thread crashes (this is what keeps the new spec's test runner alive). The legacy __onUncaughtError hook still fires for unprevented native crashes (deprecated back-compat - it is what Application.uncaughtErrorEvent has received for years). This also removes the blanket-preventDefault footgun: a generic `error` listener can no longer accidentally spare a process whose crashed thread is already gone. - Main-runtime fallback: crashes on threads with no runtime of their own (plugin/executor threads) previously got NO JS reporting at all - Runtime.isInitialized() and getCurrentRuntime() are thread-local, so the handler silently skipped them. The handler now resolves current -> main (new Runtime.getMainRuntime(), set when the workerId-0 runtime initializes) and enters the main isolate cross-thread (the JNI layer takes the v8::Locker). - Already-reported fast path: an uncaughtErrorPolicy "throw" exception (isReportedToJs) now short-circuits the handler before any work - no JS roundtrip and no eager stack rendering; the message strings are built lazily, only when the logger, debug print or error activity actually consumes them. Testing: new spec crashes a fresh runtime-less Java thread and asserts nativeuncaughterror arrives via the main-runtime fallback carrying the original Throwable, `error` never fires (invariant pin), the prevented report skips the legacy hook, and the process survives. Full suite green on an API 35 arm64 emulator: 605 specs, 0 failures.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.