software-mansion / software-mansion/react-native-screens
[iOS][New Arch] NSGenericException "Collection was mutated while being enumerated" in RNSScreenContainer.updateContainer (reentrant Fabric mounting during enumeration)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 3.7k
- Forks
- 714
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 71
Description
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: updateContainer → markChildUpdated → setActivityStateOrNil: →
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
- iOS app on the New Architecture, inside a stack navigator.
- 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. - 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
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
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in ios/RNSScreenContainer.mm at updateContainer, especially the loops around lines 158-205, and trace how detachScreen: and attachScreen: can re-enter mounting callbacks. Validate the change against the described Fabric transition scenario; done means the mutable collection is not enumerated during reentrant updates and the crash no longer reproduces.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- ios, objective-c, react-native
- Domain
- mobile
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100