getsentry / getsentry/sentry-react-native
Fatal 'JavaCallback was already settled' from getNewScreenTimeToDisplay under rapid navigation (Android, Expo, new arch)
- Dominant language
- TypeScript
- Stars
- 1.8k
- Forks
- 366
- Avg merge
- 1d 9h
- Merged PRs (30d)
- 89
Description
### OS
Android
### Platform
Hermes, New Architecture
### SDK Version
8.19.0
### React Native Version
0.85.3 (Expo SDK 56, expo-modules-core 56.0.19)
### Steps to Reproduce
Not deterministic — it is a race, but the shape that reproduces it in our production fleet:
1. `Sentry.init` with `reactNavigationIntegration({enableTimeToInitialDisplay: true})`, container registered via `registerNavigationContainer` (expo-router).
2. App with 4 native bottom tabs (`react-native-bottom-tabs`) sharing a header.
3. Switch tabs rapidly and/or push a modal right after a tab switch (our events consistently show a paywall modal being pushed immediately after navigation — breadcrumbs end with the new screen's mount logs).
### Expected Result
TTID measurement completes or is dropped silently when navigations race.
### Actual Result
Fatal crash reported through `onerror`:
```
Error: Non-js exception: JavaCallback was already settled. Cannot invoke it again
```
Event tags point at the SDK's own TurboModule call:
- `turbo_module.name: RNSentry`
- `turbo_module.method: getNewScreenTimeToDisplay`
- `mechanism: onerror`, `handled: no`, `level: fatal`, Hermes, Android 12–16
Volume: 23 users in 4 days in production (it likely existed longer — our DSN was broken before that, so the SDK could not report its own crash).
### Analysis
`reactnavigation.ts` (`updateLatestNavigationSpanWithCurrentRoute`) calls `NATIVE.getNewScreenTimeToDisplay()` on **every** navigation state change while `enableTimeToInitialDisplay` is enabled. The Android implementation appears to keep a single stored callback to be settled on the next frame; when two navigation state changes race, the same callback gets settled twice. On Expo (new architecture) the promise guard in `expo-modules-core` (`PromiseImpl` — "was already settled") turns the second settle into the fatal above.
No matching fix found in the changelog up to 8.26.0, and no existing issue mentions `getNewScreenTimeToDisplay`, so filing this one.
### Workaround
`enableTimeToInitialDisplay: false` removes the crashing path entirely (confirmed: the call is gated only by that option).
Contributor guide
Assessment
This issue has not been assessed yet.