Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Report unhandled JS errors that don't terminate the process as `unhandled` sessions (not `crashed`)

Open
#6,659 1 comment 0 reactions 0 assignees View on GitHub

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

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

Android Errors Feature Platform: React Native React-Native
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:

  • hardCrashed is set only for mechanism { handled: false, type: 'onerror' } — packages/core/src/js/misc.ts:11, computed in packages/core/src/js/wrapper.ts:196-235.
  • Android maps it directly: InternalSentrySdk.captureEnvelope(bytes, !hardCrashed) — RNSentryModuleImpl.java:500-511. When hardCrashed, maybeStartNewSession=false and the current session ends as crashed.
  • iOS: hardCrashed → store (persist for next launch), else capture — 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 over unhandled (built-in safety net). Never also call updateSessionForDroppedEventNonTerminating for 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 via captureEnvelopeNonTerminating / captureNonTerminating.
  • #4008 fix — sampling path via updateSessionForDroppedEventNonTerminating for 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 GlobalErrorBoundary over-report.
  • Phase 2 — sampling path (follow-up): unhandled events dropped by sampleRate never reach captureEnvelope, so their session would finalize exited instead of unhandled. Needs a JS-core sampled-out hook + a new bridge method updateSessionForDroppedEventNonTerminating, 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 from crashed → 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

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from getsentry/sentry-react-native

All issues in getsentry/sentry-react-native

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.