getsentry / getsentry/sentry-react-native

Fatal 'JavaCallback was already settled' from getNewScreenTimeToDisplay under rapid navigation (Android, Expo, new arch)

Open
#6,709 2 comments 0 reactions 1 assignee Claimed by @antonis View on GitHub
Bug React-Native
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.