Repository navigation
[Android][Fabric] Fatal "Cannot remove child at index" — focus re-request inside ViewGroup.removeViewInternal throws "descendant of this view" during Fabric removal (0.86.0) #57862
Description
Description
With an accessibility service enabled, removing a focused view during a Fabric mount batch reliably (if intermittently) kills the app with:
java.lang.IllegalStateException: Cannot remove child at index N from parent ViewGroup [tag],
only M children in parent. Warning: childCount may be incorrect!
at com.facebook.react.fabric.mounting.SurfaceMountingManager.removeViewAt(SurfaceMountingManager.kt:525)
Caused by: java.lang.IllegalArgumentException: parameter must be a descendant of this view
at android.view.ViewGroup.offsetRectBetweenParentAndChild(ViewGroup.java:6478)
ReactHostImpl.handleHostException then calls destroy() unconditionally, so this is fatal rather than recoverable.
Root cause
The IllegalArgumentException is not thrown by the removal itself — it comes from the accessibility event that the focus re-request fires inside removeViewInternal. Captured on device, read bottom-up:
MountItemDispatcher.dispatchMountItems / IntBufferBatchMountItem.execute
SurfaceMountingManager.removeViewAt(SurfaceMountingManager.kt:504)
ReactClippingViewManager.removeViewAt(ReactClippingViewManager.kt:68)
android.view.ViewGroup.removeViewAt(ViewGroup.java:5702)
android.view.ViewGroup.removeViewInternal(ViewGroup.java:5775)
android.view.View.rootViewRequestFocus(View.java:8827)
android.view.View.requestFocus -> ViewGroup.onRequestFocusInDescendants (recursion)
android.view.View.handleFocusGainInternal(View.java:8591)
android.view.View.onFocusChanged(View.java:8953)
android.view.View.sendAccessibilityEvent(View.java:9178)
android.view.ViewGroup.dispatchPopulateAccessibilityEventInternal(ViewGroup.java:3700) (~10 deep)
android.view.ViewGroup$ChildListForAccessibility.init(ViewGroup.java:9334)
android.view.ViewGroup$ViewLocationHolder.init(ViewGroup.java:9520)
android.view.ViewGroup.offsetDescendantRectToMyCoords(ViewGroup.java:6407)
android.view.ViewGroup.offsetRectBetweenParentAndChild(ViewGroup.java:6478) <- throws
So: the removed child held focus → removeViewInternal calls rootViewRequestFocus() → the focus gain fires an accessibility event → ChildListForAccessibility sorts children via offsetDescendantRectToMyCoords while the removed child is still mid-detach → IllegalArgumentException.
Because removeFromArray() has already run at that point, the removal has actually succeeded and the view tree is consistent. But the exception propagates out of ViewGroup.removeViewAt, and SurfaceMountingManager.removeViewAt's catch (e: RuntimeException) converts it into the fatal IllegalStateException quoted above — reporting a childCount inconsistency that isn't the real problem.
Why #56182 doesn't cover this
#56182 added a soft-catch for exactly this IllegalArgumentException — but only around addChildrenForAccessibility, i.e. the accessibility build path. main still has nothing on the removal path, and ReactViewGroup does not override any of removeViewAt / removeViews / removeViewsInLayout / removeAllViewsInLayout.
Note this is a different bug from #57800, which is the Fabric differ emitting a Remove against a wrong parent tag and throws earlier in the same method (SurfaceMountingManager.kt:444, "Unable to remove a view from a view that is not a ViewGroup", no Caused by). This one always carries the descendant of this view cause and requires an active accessibility service; that one requires neither.
Suggested fix
Mirror the #56182 pattern onto the removal path — wrap the super calls in ReactViewGroup's removal methods and soft-catch an IllegalArgumentException whose message contains descendant of this view. Since the child is already detached when it throws, swallowing leaves the tree consistent.
We currently ship precisely that as a build-time ASM transform (RN core is a prebuilt AAR for us, so a source patch would need react.internal.buildFromSource). With it in place the throw is caught, the app survives, and no Cannot remove child follows — verified on device with the stack above.
Steps to reproduce
No deterministic reproducer — it's a race between a Fabric removal and an accessibility service's tree traversal, and we could not force it with scripted input (adb shell input) even across ~20 list remounts and ~30 fling gestures. It surfaces readily in ordinary manual use. Conditions that reproduce it for us:
- Android 16 or 17 device with at least one accessibility service enabled (no TalkBack needed — a password manager's autofill service is enough).
- A long virtualized list whose rows contain heavy focusable native views (video players, WebViews), with
removeClippedSubviewsenabled so that recycling produces frequent native view removals. - Scroll the list, or remount it (e.g. by changing its
key). - Watch for
Cannot remove child at indexwith aCaused by: parameter must be a descendant of this view.
Two notes for anyone trying to reproduce: the removed view must hold focus for rootViewRequestFocus() to run, and the default logcat ring buffer rotates too fast under WebView logging to catch it after the fact (adb logcat -G 32M helps).
React Native Version
0.86.0
Affected Platforms
Runtime - Android
Areas
Fabric - The New Renderer
Output of npx @react-native-community/cli info
React Native: 0.86.0 (new architecture, bridgeless)
React: 19.2.3
Expo: 57.0.8
Hermes: bundled
Devices observed crashing: Android 16, and Android 17 (Pixel 9 Pro, CP2A.260805.005)
AGP: 8.12.0, Gradle: 9.3.1
Accessibility services active during repro: a password manager's autofill service, plus a
third-party status-bar utility (neither is TalkBack; either alone is sufficient)
— drafted by Claude (AI)
Activity
react-native-bot commentedon Aug 7, 2026
Warning
Missing reproducer: We could not detect a reproducible example in your issue report. Reproducers are mandatory and we can accept only one of those as a valid reproducer:
You can read more about about it on our website: How to report a bug.
chengjwk commentedon Aug 14, 2026
Following up with the full mechanism, plus the precondition that I think explains why this is hard to reproduce — and a candidate Playground repro built from it.
The IAE is thrown by the framework during a focus re-request, not by RN
ViewGroup.removeViewInternaldetaches the child before callingrootViewRequestFocus(). If the removed child (or a descendant) held input focus, that focus re-request fires an accessibility event whose tree walk sorts children viaoffsetDescendantRectToMyCoords— while the just-detached child is still mid-removal:Why it surfaces as a fatal
IllegalStateExceptionThe throw is synchronous inside the Fabric mount item, so it propagates into
SurfaceMountingManager.removeViewAt'scatch (e: RuntimeException)(v0.86.0,SurfaceMountingManager.kt:505) and is rethrown asIllegalStateException("Cannot remove child at index … only N children in parent")(:526).Three notes that may help triaging other reports of this ISE:
catchwraps anyRuntimeExceptionfrom the removal, so the message is the same regardless of root cause. TheCaused by:is the only discriminator.only 0 childrenis expected here, not evidence against this diagnosis.removeViewInternalhas already detached the child (count → 0) by the time the IAE fires, so RN's fallback count legitimately reports 0.AndroidRuntimeFATAL block. It crosses JNI ascom.facebook.jni.CppException: Exception in HostFunction, which flattens it; theCaused by:is logged separately byMountItemDispatcher. This is probably why many reports of this ISE arrive with no cause attached.Why #56182 doesn't cover this
#56182 added a
try/catchfor this exact IAE, but only onaddChildrenForAccessibility. I checked the source on both tags:v0.86.0(ReactViewGroup.kt:1003, message check at:1072) and0.87-stable(:989/:1058). In both, that is still the only guarded method — there is noremoveView*/removeViewsInLayoutoverride. So the removal path is unguarded in 0.87.0 as well.Counterfactual evidence that the removal path is the right level to guard
We inject an override of
ReactViewGroup.removeViewAtvia AGP bytecode instrumentation (same mechanism as the transform in #57800), wrapping only thesuper.removeViewAt(index)call in acatchforIllegalArgumentExceptionwhose message containsdescendant of this view, and rethrowing anything else.With it in place, the IAE is intercepted one frame below
SurfaceMountingManager's catch: the fatal ISE never occurs, the process survives, and the view hierarchy is intact afterwards (no missing or corrupted views from the swallowed removal). Without it, the same interaction is fatal. Mirroring #56182 onto the removal methods should be sufficient.Candidate reproducer — and the precondition I think was missing
View.sendAccessibilityEventis a no-op unless an accessibility service is running. With no a11y service enabled, this bug cannot fire at all, which I suspect is why it doesn't show up in ordinary testing. Preconditions I believe are necessary:ViewGroup.removeViewAt— i.e.removeClippedSubviewsdisabled; the clipping branch takesremoveViewWithSubviewClippingEnabledinstead (ReactClippingViewManager.kt:58-65)Playground sketch:
With TalkBack enabled, focus the
TextInput, then unmount the subtree.To be clear about the status: this snippet is untested — I'm posting the mechanism and the a11y-service precondition now in case that is enough for someone to confirm faster than I can. In our app the crash reproduces readily under real manual interaction but not under scripted
adb shell input, so the timing seems to depend on genuine focus/a11y-event interleaving. If I can make it deterministic I'll open a Playground PR as the formal reproducer.Environment
0.87-stable/ 0.87.0— drafted by Claude (AI)
chengjwk commentedon Aug 14, 2026
Update: the candidate reproducer above does not fire. Built it as a standalone RN 0.86.0 app (Fabric, bridgeless, Hermes, release build; Pixel 9 Pro / Android 17) and drove ~800 timer-driven unmounts of a focused subtree, sweeping:
TextInputinside the removed subtree, with a focusable sibling surviving outside itaccessiblechildren, removal at a middle index) so theChildListForAccessibility→ViewLocationHoldersibling sort is actually reachableuiautomator dumpin a loop) with accessibility services enabledZero IAEs, zero fatals. So the preconditions in my previous comment are necessary but evidently not sufficient. One caveat: TalkBack itself was not running — the enabled services subscribe only to
TYPE_WINDOW_STATE_CHANGED. But in the app where this crashes readily, guard hits occur with exactly those services enabled, so a broader-subscription service is not required there.The clearest structural difference in the reproducing app: the crashing parent's ancestor chain runs through fragment-backed screen containers (
react-native-screens:ScreenContentWrapper→Screen→ScreenStack), with removals arriving around navigation transitions. Possibly relevant to that:ScreenStackViewManager.getChildCountreturns the fragment count whileSurfaceMountingManager's fallback scan uses the rawchildCount, so the two can disagree during teardown.Next step on my side: instrumenting the guarded build to dump the exact hierarchy (parent class, child count/classes, ancestor chain, focus holder) at the instant the IAE is caught, then minimizing from a known-crashing configuration instead of building up from a blank app. Will report back.
— drafted by Claude (AI)