Skip to content

[iOS][New Arch] NSGenericException "Collection was mutated while being enumerated" in RNSScreenContainer.updateContainer (reentrant Fabric mounting during enumeration) #4168

@HaraldHenriksson

Description

@HaraldHenriksson

Description

On the New Architecture (iOS), -[RNSScreenContainerView updateContainer] enumerates the live
_reactSubviews array while, inside the loop body, attaching/detaching child view controllers via
detachScreen:/attachScreen:. Under Fabric, those UIKit VC-containment callbacks
(addChildViewController / removeFromParentViewController / removeFromSuperview) can
synchronously flush pending React mounting, which calls mountChildComponentView /
unmountChildComponentView[_reactSubviews insertObject:/removeObject:], mutating the array
mid-enumeration:

  *** Terminating app due to uncaught exception 'NSGenericException', reason:
  '*** Collection <__NSArrayM: 0x…> was mutated while being enumerated.'

Top frames: updateContainermarkChildUpdatedsetActivityStateOrNil:
updateProps:oldProps:RCTMountingManager synchronouslyUpdateViewOnUIThread: (driven from an
Animated screen-transition prop update).

It terminates on the main thread below JS, so JS-only crash reporters never capture it. We ship the New Architecture only, so haven't verified Paper directly — but the reentrancy window shouldn't exist there, since pre-Fabric mounting is async/batched rather than synchronous on the main thread. Present in 4.23.0 and unchanged through 4.24.0–4.25.2 and current main.

Proposed fix: enumerate an immutable snapshot (NSArray *snap = [_reactSubviews copy];).
Reentrant mutations then hit the real array, reconcile via their own markChildUpdated follow-up,
and the loop bodies are idempotent against a stale entry (detach of an already-detached screen is
a nil no-op; attach is guarded by _activeScreens). In our app this took the crash from
reproducible-every-time to zero with no behavioral change. Happy to open a PR.

Reproduction note: no minimal Snack — the crash is a timing race (a burst of synchronous
Fabric mounting must coincide with a transition driving updateContainer), impractical to reduce.
The defect is plain from the source linked below: updateContainer enumerates the mutable
_reactSubviews (loops at L163/L177/L186/L200) while reentrant VC lifecycle mutates it.

Steps to reproduce

  1. iOS app on the New Architecture, inside a stack navigator.
  2. Push a screen at the same moment a synchronous burst of React mounting occurs in the same frame
    — e.g. completing an auth flow (Redux state flip, WebSocket reconnect, several re-renders) while
    the navigator pushes the next screen.
  3. App crashes with the NSGenericException above.

Deterministic in a form/wizard that pushes a screen per step. We bisected it to be independent of
our app code (reproduces with the screen container untouched) and absent on a navigation layout
that doesn't co-locate the work with the transition. Debug builds widen the window; release builds
also crash.

Snack or a link to a repository

- (void)updateContainer
{
BOOL screenRemoved = NO;
// remove screens that are no longer active
NSMutableSet *orphaned = [NSMutableSet setWithSet:_activeScreens];
for (RNSScreenView *screen in _reactSubviews) {
if (screen.activityState == RNSActivityStateInactive && [_activeScreens containsObject:screen]) {
screenRemoved = YES;
[self detachScreen:screen];
}
[orphaned removeObject:screen];
}
for (RNSScreenView *screen in orphaned) {
screenRemoved = YES;
[self detachScreen:screen];
}
// detect if new screen is going to be activated
BOOL screenAdded = NO;
for (RNSScreenView *screen in _reactSubviews) {
if (screen.activityState != RNSActivityStateInactive && ![_activeScreens containsObject:screen]) {
screenAdded = YES;
}
}
if (screenAdded) {
// add new screens in order they are placed in subviews array
NSInteger index = 0;
for (RNSScreenView *screen in _reactSubviews) {
if (screen.activityState != RNSActivityStateInactive) {
if ([_activeScreens containsObject:screen] && screen.activityState == RNSActivityStateTransitioningOrBelowTop) {
// for screens that were already active we want to mimick the effect UINavigationController
// has when willMoveToWindow:nil is triggered before the animation starts
[self prepareDetach:screen];
} else if (![_activeScreens containsObject:screen]) {
[self attachScreen:screen atIndex:index];
}
index += 1;
}
}
}
for (RNSScreenView *screen in _reactSubviews) {
if (screen.activityState == RNSActivityStateOnTop) {
[screen notifyFinishTransitioning];
}
}

Screens version

4.23.0

React Native version

0.83.4

Platforms

iOS

JavaScript runtime

Hermes

Workflow

Expo managed workflow

Architecture

Fabric (New Architecture)

Build type

Release mode

Device

Real device

Device model

iPhone 15 Pro (iOS 26)

Acknowledgements

Yes

Metadata

Metadata

Assignees

No one assigned

    Labels

    missing-reproThis issue need minimum repro scenarioplatform:iosIssue related to iOS part of the library

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions