Report unhandled JS errors that don't terminate the process as `unhandled` sessions (not `crashed`)
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 52/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- java, javascript, objective-c, react-native, swift, typescript
- Domain
- mobile, observability-sre
Research direction
Start with reactnativeerrorhandlers.ts, misc.ts, NativeRNSentry.ts, wrapper.ts, RNSentryModuleImpl.java, RNSentry.mm, and RNSentryInternal.swift, tracing the existing hardCrashed path from the GlobalErrorBoundary decision to native capture. Verify the Android and iOS non-terminating APIs and their session semantics. Done means survived fatal errors become unhandled while terminating crashes remain crashed, with the bridge change backward-compatible.
Written by the indexing model from the issue text.
Description
Summary
Adopt the native non-terminating session APIs so an unhandled fatal JS error that does not terminate the process is reported with session status unhandled instead of crashed. In practice this is the case where a mounted GlobalErrorBoundary catches a fatal and keeps the app running. Genuine terminating crashes stay crashed, unchanged.
Targeted at the next RN SDK major — not because it is an API/ABI break (it isn't), but because it shifts a visible Release Health metric (crash-free session rate). See Release classification below.
Spec basis
Per the sessions spec:
crashed— "session terminated in a crash (process terminated)"unhandled(spec v1.6.0) — "an unhandled error occurred but the process did not terminate", e.g. "the language or framework prevented termination"
GlobalErrorBoundary is exactly that framework mechanism, so a survived fatal is an unhandled session by definition. Reporting it as crashed is a spec deviation. (sentry-conventions is span/attribute naming only — neutral here.)
Current RN behavior & the confirmed over-report
RN has no dedicated session API over the bridge; a hard crash is signaled by the hardCrashed boolean on captureEnvelope:
hardCrashedis set only for mechanism{ handled: false, type: 'onerror' }—packages/core/src/js/misc.ts:11, computed inpackages/core/src/js/wrapper.ts:196-235.- Android maps it directly:
InternalSentrySdk.captureEnvelope(bytes, !hardCrashed)—RNSentryModuleImpl.java:500-511. WhenhardCrashed,maybeStartNewSession=falseand the current session ends ascrashed. - iOS:
hardCrashed→store(persist for next launch), elsecapture—RNSentry.mm:624-646.
reactnativeerrorhandlers.ts sets { handled: false, type: 'onerror' } for all isFatal errors. But when a GlobalErrorBoundary fallback is mounted, the handler deliberately skips defaultHandler (reactnativeerrorhandlers.ts:242-244) so the app survives — yet the session is still reported crashed. That is the over-report, and it is a gap the SDK created itself by shipping GlobalErrorBoundary.
Unhandled promise rejections are already marked handled: true and are unaffected.
Native APIs (available now, both platforms bundled)
- Android — sentry-java 8.55.0 (#5921), merged via #6658:
InternalSentrySdk.captureEnvelopeNonTerminating(byte[]),Session.State.Unhandled. - iOS — sentry-cocoa 9.27.0 (already pinned in
RNSentry.podspec):SentrySDK.internal.envelope.captureNonTerminating(_:). - Both: keep the same session id, bump the error count, mark the session
unhandled, and a later crash or abnormal exit still takes precedence overunhandled(built-in safety net). Never also callupdateSessionForDroppedEventNonTerminatingfor the same event (double-count).
Flutter precedent — and why RN diverges
Flutter is adopting the same native APIs as a stacked series (fixes sentry-dart#3300), all opened 2026-09-07:
- #4006 — bump native SDKs (Android 8.55.0 + Cocoa 9.27.0).
- #4007
feat— capture path viacaptureEnvelopeNonTerminating/captureNonTerminating. - #4008
fix— sampling path viaupdateSessionForDroppedEventNonTerminatingfor sampled-out unhandled events.
Both Flutter PRs are marked "No breaking changes"; #3300 is labeled Improvement. The work is grouped into Flutter's v10 for release-timing reasons (a visible metric change on a major boundary), not because the code breaks compatibility.
Key divergence: Flutter pushes the decision fully into native by inspecting mechanism.handled == false, because in Flutter an unhandled framework error never terminates the process. RN is different — an unhandled onerror fatal does terminate (RCTFatal) unless a GlobalErrorBoundary catches it. So RN must not copy Flutter's native-decides model (it would mislabel real JS crashes as unhandled and lose crash reporting). RN keeps the survival signal in JS and carries it across the bridge.
Proposed design
| # | Module | Change |
|---|---|---|
| M1 | integrations/reactnativeerrorhandlers.ts |
Owns the survival decision. When isFatal and an interested GlobalErrorBoundary subscriber is present (the path where defaultHandler is skipped), annotate the event's exception mechanism as non-terminating. Every other fatal stays crashed. |
| M2 | misc.ts |
Reshape isHardCrash(payload) → getSessionDisposition(payload): 'crashed' | 'unhandled' | 'ok', read from the serialized envelope payload (the only channel to the wrapper). |
| M3 | NativeRNSentry.ts + wrapper.ts |
Additive nonTerminating: boolean on captureEnvelope options, beside hardCrashed (mutually exclusive). Additive key on an untyped Object param → backward-compatible ABI; an older cached native binary ignores it and degrades to a normal capture. |
| M4 | RNSentryModuleImpl.java |
nonTerminating → InternalSentrySdk.captureEnvelopeNonTerminating(bytes); else the existing path. Runs off-main (@ReactMethod), safe for the API's synchronous persist. |
| M5 | RNSentry.mm + RNSentryInternal.swift |
Add RNSentryInternal.captureNonTerminating(_:) → SentrySDK.internal.envelope.captureNonTerminating(envelope); branch on nonTerminating before the store/capture fork. |
Phasing
- Phase 1 — capture path (this issue): M1–M5. Mirrors Flutter #4007; closes the
GlobalErrorBoundaryover-report. - Phase 2 — sampling path (follow-up): unhandled events dropped by
sampleRatenever reachcaptureEnvelope, so their session would finalizeexitedinstead ofunhandled. Needs a JS-core sampled-out hook + a new bridge methodupdateSessionForDroppedEventNonTerminating, with a no-double-count guard. Mirrors Flutter #4008 — hold until that design settles.
Release classification
- Not an API/ABI break. No public JS API change; the bridge change is additive and backward-compatible.
- It is a Release Health behavior shift —
GlobalErrorBoundary-survived errors move fromcrashed→unhandled, raising crash-free session rate for affected apps. Ship in the next major, with a prominent CHANGELOG entry and a docs note so users understand the metric change.
Open risk
M1's survival check is re-evaluated post-flush, but the envelope is captured pre-flush → the disposition is a best-effort prediction. Worst case is a rare crashed↔unhandled mislabel, self-corrected by the "later crash takes precedence" rule. Never a host-app crash.
Follow-up from the 8.55.0 bump (#6658).
- Dominant language
- TypeScript
- Stars
- 1.8k
- Forks
- 369
- Avg merge
- 20h 55m
- Merged PRs (30d)
- 105
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from getsentry/sentry-react-native
-
React-Native Waiting for: Product Owner
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
getsentry/sentry-react-native#6822 · 1 comment ·
Maintainers usually reply within 1 day
-
Bug React-Native Waiting for: Product Owner
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
getsentry/sentry-react-native#6821 · 1 comment ·
Maintainers usually reply within 1 day
-
React-Native Replays Task
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
getsentry/sentry-react-native#6680 · 1 comment ·
Maintainers usually reply within 1 day
-
Improvement React-Native
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
getsentry/sentry-react-native#6143 · 2 comments ·
Maintainers usually reply within 1 day
-
React-Native Task User Feedbacks
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
getsentry/sentry-react-native#5932 · 2 comments ·
Maintainers usually reply within 1 day
All issues in getsentry/sentry-react-native
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
aiko-chan-ai/DiscordBotClient#380 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
vercel/ai-elements#507 ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Maintainers usually reply within 2 days
-
Difficulty 2/5 Half a day Newbie friendliness 84/100
anaclumos/qa-interns#148 · 1 comment ·
Maintainers usually reply within 1 day