Skip to content

[Due for payment 2026-10-21] [$175] [Sentry: APP-M0V] Android crash, IllegalStateException Fragment already added in ScreenStackFragment #102046

Open

Description

@mountiny

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


Version Number: 9.4.89
Reproducible in staging?: Needs to be confirmed
Reproducible in production?: Yes — crash is live in production
If this was caught during regression testing, add the test name, ID and link from BrowserStack: N/A — found via Sentry production crash scan
Email or phone of affected tester (no customers): N/A
Logs: https://stackoverflow.com/c/expensify/questions/4856
Expensify/Expensify Issue URL:
Issue reported by: Sentry production crash scan
Slack conversation (hyperlinked to channel name): N/A

Action Performed:

  1. Open the Expensify app on Android (HybridApp).
  2. Trigger a screen that presents a modal over a navigation transition. The reported view is the AI features promo modal, shown shortly after the app starts.
  3. Navigate while that modal is appearing or dismissing.

Expected Result:

The modal and the navigation transition both complete. No crash.

Actual Result:

The app crashes on the main thread:

IllegalStateException: Fragment already added: ScreenStackFragment{...}

react-native-screens runs a fragment transaction that adds a fragment already present in the fragment store.

Workaround:

Unknown. The app restarts.

Platforms:

Select the officially supported platforms where the issue was reproduced:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome / Safari

Screenshots/Videos

Sentry details

Sentry issue: https://expensify.sentry.io/issues/APP-M0V

Impact (snapshot at filing, 2026-09-23):

  • Users (last 7d): 12
  • Events (last 7d): 58
  • First seen: 2026-09-16
  • Last seen: ongoing
  • Platform: Android native, HybridApp (org.me.mobiexpensifyg), Android 16, Samsung flagship hardware
  • View at crash time: Dynamic_AIFeaturesPromoModal_Root
  • Mechanism: UncaughtExceptionHandler, level fatal
  • Substatus: escalating

Stack trace (first-party and library frames, innermost last):

com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.kt:21)
com.swmansion.rnscreens.ScreenContainer.onScreenChanged$lambda$8(ScreenContainer.kt:358)
com.swmansion.rnscreens.ScreenContainer.performUpdates(ScreenContainer.kt:375)
com.swmansion.rnscreens.ScreenStack.onUpdate(ScreenStack.kt:303)
androidx.fragment.app.BackStackRecord.commitNowAllowingStateLoss(BackStackRecord.java:323)
androidx.fragment.app.FragmentManager.execSingleAction(FragmentManager.java:2002)
androidx.fragment.app.FragmentManager.addFragment(FragmentManager.java:1733)
androidx.fragment.app.FragmentStore.addFragment(FragmentStore.java:93)

Suspected cause: a race between a modal presentation and a navigation update. ScreenStack.onUpdate computes the set of fragments to attach, then performUpdates commits the transaction on the next handler tick. If a second update lands in between, the same ScreenStackFragment gets queued for addition twice, and FragmentStore.addFragment throws.

Worth checking the react-native-screens version in use against upstream fixes for duplicate fragment attachment, and whether the AI features promo modal mounts during a navigation transition rather than after it settles.

View all open jobs on GitHub

Issue OwnerCurrent Issue Owner: @nyomanjyotisa
Upwork Automation - Do Not Edit

Activity

  1. self-assigned this
    on Sep 23, 2026
  2. added
    ExternalAdded to denote the issue can be worked on by a contributor
    BugSomething is broken. Auto assigns a BugZero manager.
    on Sep 23, 2026
  3. melvin-bot commented on Sep 23, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @nyomanjyotisa (External)

  4. melvin-bot commented on Sep 23, 2026

    @melvin-bot

    Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.

  5. MelvinBot commented on Sep 23, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    A missing isAdded guard in react-native-screens@4.25.0, on the Android code path that only runs when the top screen is translucent. The AI features promo modal is exactly that kind of screen, which is why it dominates this Sentry signal.

    In ScreenStack.onUpdate, when the screen underneath a translucent screen is not attached, every fragment from visibleBottom upward is added to the transaction with no check of whether it is already attached:

    if (visibleBottom != null && !visibleBottom.fragment.isAdded) {
        screenWrappers.asSequence()
            .dropWhile { it !== visibleBottom }
            .forEach { wrapper -> transaction.add(id, wrapper.fragment)... }   // no isAdded check
    } else if (newTop != null && !newTop.fragment.isAdded) {                   // sibling branch IS guarded

    So if a navigation update detaches the screen below the modal while the modal's own fragment stays attached, transaction.add is called on an already-added ScreenStackFragment, and FragmentStore.addFragment throws on commit. The sibling newTop branch two lines below guards correctly — the asymmetry is the bug.

    visibleBottom is non-null only when the top screen is translucent, and Screen.isTranslucent() is true for transparentModal. The AI promo modal is registered with rootNavigatorScreenOptions.centeredModalNavigator, which sets presentation: Presentation.TRANSPARENT_MODAL.

    This is a known upstream bug: Crash on Android: "Fragment already added" in ScreenStack.onUpdate with transparentModal after background/resume, with an open, unmerged fix in fix(Android): guard against re-adding an attached fragment in ScreenStack.onUpdate.

    Evidence: stack trace lines map exactly onto the installed 4.25.0 source

    Every frame in the Sentry trace lines up with the code we actually ship (react-native-screens": "4.25.0" in package.json:217):

    Sentry frame 4.25.0 source
    ScreenContainer.onScreenChanged$lambda$8(ScreenContainer.kt:358) performUpdates() inside runOnUiQueueThread
    ScreenContainer.performUpdates(ScreenContainer.kt:375) onUpdate()
    ScreenStack.onUpdate(ScreenStack.kt:303) transaction.commitNowAllowingStateLoss() — commits the transaction built at L267–L283

    The modal's fragment is never removed earlier in that same transaction: the two transaction.remove passes cover wrappers that left screenWrappers/were dismissed, and wrappers below visibleBottom. The modal sits above visibleBottom, so it reaches add still attached.

    The upstream reporter on #4156 hit the same crash from the Paper ScreensShadowNode entry point; we hit it from the Fabric runOnUiQueueThread entry point. Same crash site, same missing guard. Their device profile (Samsung / Pixel, Android 16) matches ours.

    Note #4445 in the same repo is a different crash — ScreenFragment in ScreenContainer.onUpdate, caused by a deferred-commit workaround that App does not have.

    What changes do you think we should make in order to solve the problem?

    Add a patch-package patch to react-native-screens@4.25.0 that mirrors upstream PR #4157: in the translucent re-attach branch of ScreenStack.onUpdate, filter the sequence to skip wrappers whose fragment is already added, exempting dismissed wrappers (their remove is queued earlier in the same transaction, so remove-then-add is the legal re-attach path).

    Follow the existing convention in patches/react-native-screens/:

    1. Name it react-native-screens+4.25.0+002+fix-fragment-already-added-in-screenstack-onupdate.patch.
    2. Add a details.md entry recording the reason, the upstream PR, this issue, and the removal condition — drop the patch once we bump to the first release containing the merged Add modal for existing owners error #4157.

    Because the crash needs a real device race, validate by checking that the Sentry event rate for APP-M0V goes to zero after the build ships, rather than expecting a local repro.

    What alternative solutions did you explore? (Optional)

    • Bump react-native-screens to 4.28.0. I checked the 4.28.0 source — this does not fix it. 4.28.0 added a filter to that loop, but it only excludes dismissed and INACTIVE wrappers. An attached, active translucent fragment still passes through to transaction.add and still throws. Add modal for existing owners error #4157 is unmerged, so no published release carries the guard.
    • Stop using transparentModal for the promo modal. Would sidestep the branch for one screen but leave every other centeredModalNavigator / basicModalNavigator screen exposed, and it changes product behaviour to work around a library bug.
    • Delay the promo modal further. useAIFeaturesPromoModal already defers behind TransitionTracker.runAfterTransitions({waitForUpcomingTransition: true}) and waitForProtectedRoutes(). TransitionTracker tracks JS-side transition events, not Android fragment-transaction state, so more deferral would only shift the odds — it cannot close the race.

    Related issues

    I found no duplicate or related open issue in Expensify/App for this crash.


    Note: I could not verify this in a browser — browser control was unavailable for this run, and this is an Android-native crash that a web session could not reproduce anyway. The analysis is from static analysis of the shipped react-native-screens@4.25.0 source, the Sentry stack trace, and upstream issue reports.


    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/102046) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run

  6. nyomanjyotisa commented on Sep 28, 2026

    @nyomanjyotisa
    Member

    @MelvinBot The guard stops the crash but hides the open modal in my isolated Android repro. Removing and re-adding attached, non-dismissed fragments preserved it. Could you update the proposal?

  7. MelvinBot commented on Sep 28, 2026

    @MelvinBot
    Contributor

    Proposal (updated)

    Updated 2026-09-28: Corrected based on feedback from nyomanjyotisa. The fix now removes and re-adds attached fragments instead of skipping them.

    What is the root cause of that problem?

    The crash cause is the same. In react-native-screens@4.25.0, the translucent re-attach branch of ScreenStack.onUpdate calls transaction.add for every screen from visibleBottom upward. It never checks isAdded. When a navigation update detaches the screen under the AI promo modal (a transparentModal), the modal's fragment is still attached. Adding it again throws Fragment already added.

    Why skipping the add hides the modal: the stack's drawing order depends on the order in which fragments are added.

    • FragmentManager appends each added fragment's view to the end of ScreenStack's children.
    • ScreenStack draws its children in child order.
    • If you skip the modal, it keeps its old child index. visibleBottom gets appended after it and draws on top of it.
    • The runOnCommit { top?.screen?.bringToFront() } call does not fix the order. screen is a child of the fragment's ScreensCoordinatorLayout (ScreenStackFragment.kt:207), not of ScreenStack. It only moves the screen inside that wrapper.

    What changes do you think we should make in order to solve the problem?

    Patch react-native-screens@4.25.0 with patch-package. In the translucent re-attach loop, queue a remove before the add for any fragment that is already attached and not dismissed:

    .forEach { wrapper ->
        if (wrapper.fragment.isAdded && !dismissedWrappers.contains(wrapper)) {
            transaction.remove(wrapper.fragment)
        }
        transaction.add(id, wrapper.fragment).runOnCommit { top?.screen?.bringToFront() }
    }

    This fixes both problems:

    1. No fragment is added twice, so the crash stops.
    2. Every screen from visibleBottom upward is added again in stack order, so the modal's view ends up last and draws on top.

    Dismissed wrappers skip the extra remove because the first pass in the same transaction already removes them. The remove-then-add pattern is the same re-attach path those wrappers use today.

    Follow the conventions in patches/react-native-screens/:

    1. Name the file react-native-screens+4.25.0+002+fix-fragment-already-added-in-screenstack-onupdate.patch.
    2. Add a details.md entry with the reason, this issue, and a link to upstream #4157. Note that its skip-only guard causes the hidden-modal regression. Remove our patch once upstream ships a fix that keeps the draw order.

    Verification:

    • In your isolated repro, confirm there is no crash and the modal stays visible on top.
    • Confirm the re-added modal does not replay its enter animation.
    • After release, confirm the APP-M0V event rate drops to zero.

    What alternative solutions did you explore? (Optional)

    • Skip already-attached fragments (upstream Add modal for existing owners error #4157, my previous proposal). Rejected. It stops the crash but draws the modal under visibleBottom, as your repro showed.
    • Bump to react-native-screens@4.28.0. Its added filter only excludes dismissed and INACTIVE wrappers. An attached, active modal still gets added twice.
    • Stop using transparentModal for the promo modal. This protects only one screen and changes product behavior to work around a library bug.

    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/102046) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run

  8. nyomanjyotisa commented on Sep 29, 2026

    @nyomanjyotisa
    Member

    @MelvinBot The remove/re-add fix prevented the crash and kept the modal visible in my isolated 4.25.0 Android test. App now uses 4.28.0. Could you update the proposal for that version?

  9. MelvinBot commented on Sep 29, 2026

    @MelvinBot
    Contributor

    Proposal (updated for react-native-screens@4.28.0)

    Updated 2026-09-29: Retargeted to 4.28.0, based on feedback from nyomanjyotisa. The fix is the same remove/re-add approach you validated on 4.25.0.

    What is the root cause of that problem?

    4.28.0 still has the bug. In the translucent re-attach loop of ScreenStack.onUpdate, every wrapper from visibleBottom upward goes to transaction.add without an isAdded check. When the screen under the AI promo modal (a transparentModal) is detached but the modal stays attached, the modal is added twice. The commit then throws Fragment already added.

    4.28.0 only added a filter for dismissed and INACTIVE wrappers (upstream #4571). An attached, active modal still passes the filter. The upstream skip-only fix, fix(Android): guard against re-adding an attached fragment in ScreenStack.onUpdate, is still open, and no release after 4.28.0 includes a fix.

    What changes do you think we should make in order to solve the problem?

    Add a patch-package patch for 4.28.0. In that loop, remove an attached fragment before adding it again:

    .filter { !dismissedWrappers.contains(it) && it.screen.activityState !== Screen.ActivityState.INACTIVE }
    .forEach { wrapper ->
        // Remove and re-add attached fragments so they're re-appended in stack order and the translucent top stays on top.
        if (wrapper.fragment.isAdded) {
            transaction.remove(wrapper.fragment)
        }
        // TODO: It should be enough to dispatch this on commit action once.
        transaction.add(id, wrapper.fragment).runOnCommit {
            top?.screen?.bringToFront()
        }
    }

    The change from the 4.25.0 version: the !dismissedWrappers.contains(wrapper) check is gone. 4.28.0 already filters dismissed wrappers out of this loop, so the check can never be false. No wrapper in this loop is removed earlier in the same transaction, so nothing gets removed twice.

    The 4.28.0 bump in Bump react-native-screens to 4.28.0 and drop the patch deleted patches/react-native-screens/. So:

    1. Create patches/react-native-screens/react-native-screens+4.28.0+001+fix-fragment-already-added-in-screenstack-onupdate.patch.
    2. Recreate patches/react-native-screens/details.md in the standard format (see patches/react-native-reanimated/details.md). Link this issue and upstream Adding a space before the search term is giving totally different results #4156 / Add modal for existing owners error #4157. Note that Add modal for existing owners error #4157's skip-only guard hides the modal. Remove the patch once upstream ships a fix that keeps the draw order.
    Verification
    • Repeat your isolated repro on 4.28.0: no crash, and the modal stays visible on top.
    • Recheck the two flows the bump PR relied on for 🍒 Cherry pick PR #4568 to staging 🍒 #4571: the RHP and the attachment modal over an opaque screen, including dismissing them.
    • Confirm the re-added modal does not replay its enter animation.
    • After release, confirm the APP-M0V event rate drops to zero.

    I did not run this on a device. The crash needs a native timing race, and the tester can't apply patch-package patches. The code above is based on the 4.28.0 source at the release tag.

    What alternative solutions did you explore? (Optional)

    • Keep the 4.25.0 condition unchanged. It works, but the dismissed check is dead code on 4.28.0.
    • Skip attached fragments (upstream Add modal for existing owners error #4157). Stops the crash but hides the modal, as your earlier repro showed.
    • Wait for a newer release. The latest releases are 4.28.0 and 5.0.0-alpha.3. Neither contains a fix.

    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/102046) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run

  10. mountiny commented on Oct 1, 2026

    @mountiny
    ContributorAuthor

    @nyomanjyotisa how is this looking?

  11. 5 remaining items

  12. melvin-bot commented on Oct 5, 2026

    @melvin-bot

    Current assignee @mountiny is eligible for the choreEngineerContributorManagement assigner, not assigning anyone new.

  13. mountiny commented on Oct 5, 2026

    @mountiny
    ContributorAuthor

    @nyomanjyotisa please go ahead with the proposal from Melvin then

  14. nyomanjyotisa commented on Oct 6, 2026

    @nyomanjyotisa
    Member

    @MelvinBot please implement your revised proposal.

  15. MelvinBot commented on Oct 6, 2026

    @MelvinBot
    Contributor

    🤖 I created a draft PR: Patch react-native-screens to fix Fragment already added crash on Android
    Please mark it as "Ready for review" when it is ready for review.

    It implements the revised proposal for react-native-screens@4.28.0. The PR adds two files:

    Checks
    • The patch applies cleanly to the upstream 4.28.0 ScreenStack.kt. The installed node_modules copy matches that tag byte for byte.
    • The patch is listed in details.md, which is the check validatePatches.sh runs.
    • I did not run the app on a device. nyomanjyotisa, please fill out the Tests and QA sections from your Android before/after test.

    view run

  16. added
    ReviewingHas a PR in review
    and removed on Oct 7, 2026
  17. changed the title [-][Sentry: APP-M0V] Android crash, IllegalStateException Fragment already added in ScreenStackFragment[/-] [+][$175] [Sentry: APP-M0V] Android crash, IllegalStateException Fragment already added in ScreenStackFragment[/+] on Oct 11, 2026
  18. melvin-bot commented on Oct 11, 2026

    @melvin-bot
  19. changed the title [-][$175] [Sentry: APP-M0V] Android crash, IllegalStateException Fragment already added in ScreenStackFragment[/-] [+][Due for payment 2026-10-21] [$175] [Sentry: APP-M0V] Android crash, IllegalStateException Fragment already added in ScreenStackFragment[/+] on Oct 14, 2026
  20. melvin-bot commented on Oct 14, 2026

    @melvin-bot

    @nyomanjyotisa

    The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.8-5 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:

    If no regressions arise, payment will be issued on 2026-10-21. 🎊

    The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorOverdue

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions