[Android] ANR: synchronouslyUpdateUIProps prints full stack traces on the UI thread when the view tag is unmounted (RN 0.86 reflection path)
[!NOTE] This issue was authored by AI on behalf of @mikelros.
Description
We received a production ANR cluster (Android, release builds, ~150 users in two weeks) whose main-thread stack ends inside Reanimated's exception logging during the draw pass:
at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:6856)
...
at com.horcrux.svg.SvgView.onDraw(SvgView.java:135)
...
at com.horcrux.svg.VirtualView.setClientRect(VirtualView.java:614)
at com.facebook.react.uimanager.events.FabricEventDispatcher.dispatchEvent(FabricEventDispatcher.kt:46)
at com.swmansion.reanimated.NodesManager.onEventDispatch(NodesManager.kt:212)
at com.swmansion.reanimated.NodesManager.performOperationsRespectingDrawPass(NodesManager.kt:132)
at com.swmansion.reanimated.NodesManager.performNonLayoutOperations(NodesManager.kt:126)
at com.swmansion.reanimated.NativeProxy.performNonLayoutOperations(NativeProxy.kt)
at com.swmansion.reanimated.NativeProxy.synchronouslyUpdateUIProps(NativeProxy.kt:253)
at com.swmansion.reanimated.nativeProxy.SynchronousPropsBufferParser.parse(SynchronousPropsBufferParser.kt:257)
at com.swmansion.reanimated.NativeProxy.synchronouslyUpdateUIProps$lambda$6(NativeProxy.kt:258)
at io.sentry.android.core.SentryLogcatAdapter.w(SentryLogcatAdapter.java:115)
at android.util.Log.w(Log.java:229)
at android.util.Log.printlns(Log.java:500)
at java.lang.Throwable.printStackTrace(Throwable.java:753)
...Code-path analysis
The code below is from 4.5.3, but main has the same shape today.
On React Native 0.86,
NativeProxy.synchronouslyUpdateUIPropstakes the new reflection path intoMountingManager.updatePropsSynchronously:// NativeProxy.kt:253-264 SynchronousPropsBufferParser.parse(intBuffer, doubleBuffer) { viewTag, props -> if (BuildConfig.IS_REACT_NATIVE_86_OR_NEWER) { try { updatePropsSynchronouslyMethod.invoke(mountingManager, viewTag, props) } catch (e: Exception) { Log.w("Reanimated", "synchronouslyUpdateUIProps failed for tag $viewTag", e) } } else { ... } }MountingManager.updatePropsSynchronouslythrowsRetryableMountingLayerExceptionwhen the tag has noSurfaceMountingManager(the view was unmounted). The vanilla RN path (FabricUIManager.synchronouslyUpdateViewOnUIThread) checksgetViewExists(reactTag)first and defers quietly — it never throws for a missing tag. The reflection path skips that guard.The
catchlogs the full throwable on the UI thread. The chain isInvocationTargetException+ cause (~160 frames in our app), printed viaThrowable.printStackTraceinside the draw pass, once per stale tag, and again on every following frame because the update is re-scheduled. Apps that observe logcat (Sentry'sSentryLogcatAdapterbreadcrumbs in our case) pay extra on top. The main thread spends whole seconds inLog.printlnsand Android raises an ANR.The synchronous re-entry during draw comes from
react-native-svg:VirtualView.setClientRectdispatches its layout event from insideonDraw, which entersperformOperationsRespectingDrawPass→performNonLayoutOperations→synchronouslyUpdateUIProps. Any other in-draw synchronous dispatch would do the same; svg only supplies the trigger.
Expected behavior
A prop update for an unmounted tag should be dropped (or deferred) silently, as the vanilla RN path does — not logged with a full stack print per tag per frame on the UI thread. We ship this as a patch-package patch and it removes the stall:
private val getViewExistsMethod by lazy {
mountingManager.javaClass.methods
.first { it.name == "getViewExists" && it.parameterTypes.size == 1 }
.apply { isAccessible = true }
}
// in synchronouslyUpdateUIProps:
try {
if (getViewExistsMethod.invoke(mountingManager, viewTag) == true) {
updatePropsSynchronouslyMethod.invoke(mountingManager, viewTag, props)
}
} catch (e: Exception) {
Log.w("Reanimated", "synchronouslyUpdateUIProps failed for tag $viewTag: $e")
}Happy to open a PR in this shape.
Occurrence details
- Two Sentry ANR groups (foreground + background), 163 events / ~150 users in two weeks, all starting with our first RN 0.86 + Reanimated 4.5.3 release. Identical main-thread stack in every sampled event.
- Devices: broad spread (Samsung, Xiaomi, others), Android 14–16, arm64-v8a, Play Store installs, not rooted.
- App: React Native 0.86.2 (New Architecture), Hermes, Expo SDK 57 (bare / dev-client workflow), release builds, expo-updates enabled.
Steps to reproduce
- Render a screen where an SVG view (
react-native-svg15.x) draws while a Reanimated shared value updates props of a view that has since unmounted (a list item with an animated style that leaves the window keeps its shared value updating). - During the draw pass, the svg layout event re-enters Reanimated,
synchronouslyUpdateUIPropsruns, the reflectiveupdatePropsSynchronouslythrowsRetryableMountingLayerExceptionfor the stale tag, and a full stack trace prints to logcat. - Watch logcat fill with
synchronouslyUpdateUIProps failed for tag ...warnings once per frame; with enough stale tags (or a logcat observer attached) the main thread stalls past the ANR threshold.
Snack or a link to a repository
No minimal repro yet — it needs a stale animated tag, which depends on app-specific unmount timing. The defect is visible by inspection: any RetryableMountingLayerException from the reflective call is logged with a full UI-thread stack print, per tag, per frame, with no back-off, while the equivalent RN path (FabricUIManager.synchronouslyUpdateViewOnUIThread) never throws for missing tags.
Reanimated version
4.5.3
Worklets version
0.11.3
React Native version
0.86.2
Platforms
Android
JavaScript runtime
Hermes
Workflow
Expo Dev Client (bare workflow)
Architecture
New Architecture (Fabric renderer)
Source: software-mansion/react-native-reanimated