You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[$175] [Sentry: APP-M66] Android: ReactModalHostView updateSystemAppearance "dialog must exist" #103778
A posted updateSystemAppearance runs after the modal dialog is already dismissed, so React Native throws 'dialog must exist when we call updateProperties'.
Reproduction
Unknown. See the events linked from the Sentry issue.
changed the title [-][Sentry: APP-M66] Android: ReactModalHostView updateSystemAppearance "dialog must exist"[/-][+][$175] [Sentry: APP-M66] Android: ReactModalHostView updateSystemAppearance "dialog must exist"[/+]on Oct 12, 2026
Our own React Native patch react-native+0.86.0+019+fix-modal-transparent-navigation-bar.patch (added in #69004) adds window.decorView.post { updateSystemAppearance() } inside ReactModalHostView.showOrUpdate, right after newDialog.show().
The post runs on a later UI-thread tick. If the modal is dismissed in between (common with quick-closing Report screen modals), dismiss() has already set the dialog field to null. updateSystemAppearance then does a checkNotNull on dialog and throws, which matches the trace (showOrUpdate$lambda$5 -> updateSystemAppearance). The unpatched code only called it synchronously right after show(), so it could not hit this.
What changes do you think we should make in order to solve the problem?
Update patch 019 so the deferred callback is safe once the modal is gone, and regenerate the patch file with patch-package.
Heart of the change:
window.decorView.post {
val d = dialog
if (d !=null&& d.isShowing) updateSystemAppearance()
}
Optionally also have the post callback skip if the decorView is no longer attached (window.decorView.isAttachedToWindow).
Update the 019 entry in patches/react-native/details.md to mention the guard.
Testing: there's no JS unit test for this native patch. Verify manually on Android by opening a modal on a report and dismissing it immediately (rapid open/close), and check there's no crash.
What alternative solutions did you explore? (Optional)
Wrap the call in try/catch: hides the error rather than checking the state, so worse.
Make updateSystemAppearance itself return early when dialog is null: also fine, but it changes more upstream code than needed, and the posted lambda is the only caller that runs late.
Remove the post: it would bring back the nav bar icon style bug that patch 019 was fixing.
A modal opens, and showOrUpdate() posts the runnable to the dialog's decor view.
The modal closes before the next frame, for example on a quick open and close or on unmount. dismiss() sets dialog = null.
The posted runnable runs. updateSystemAppearance() starts with checkNotNull(dialog) { "dialog must exist when we call updateProperties" }, which throws IllegalStateException and crashes the app.
Evidence
The Sentry frame showOrUpdate$lambda$5 (ReactModalHostView.kt:348) matches line 348 of the patched file, which holds the posted call. The crash is in the lambda, not in the direct updateSystemAppearance() call one line above it.
In the patched file, dismiss() sets dialog = null at line 210, and updateSystemAppearance() runs checkNotNull(dialog) at line 440. Nothing cancels the posted runnable.
The PR merged on 2026-09-08. Sentry first saw the crash on 2026-09-21 in 9.5.5, the first release that shipped it.
The other call the patch adds, if (dialog.isShowing) updateSystemAppearance() in updateProperties(), is safe because it runs synchronously while dialog is non-null.
What changes do you think we should make in order to solve the problem?
In patch 019, update the posted runnable so it calls updateSystemAppearance() only if dialog still exists and is showing when the runnable runs. Otherwise, it does nothing. Make this a small change to the patch itself, and keep the patch header in patches/react-native/details.md accurate.
This keeps the navigation bar fix from the PR above and removes the crash.
What alternative solutions did you explore? (Optional)
Make updateSystemAppearance() return early when dialog is null, instead of using checkNotNull. This also works, but it changes upstream behavior for every caller and could hide real bugs.
Keep a reference to the Runnable and call removeCallbacks in dismiss(). This is more precise but adds more code to the patch for the same result.
Next Steps for Contributor+ team: To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/103778) to create a draft PR. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's proposal.
Please re-state the problem that we are trying to solve in this issue.
On Android, a Sentry crash (APP-M66) occurs with java.lang.IllegalStateException: dialog must exist when we call updateProperties inside ReactModalHostView.updateSystemAppearance.
When a modal is rapidly closed or unmounted immediately after opening, dismiss() runs before the posted runnable executes on the looper queue, setting dialog = null. When the posted runnable subsequently runs, updateSystemAppearance() executes:
checkNotNull(dialog) { "dialog must exist when we call updateProperties" }
which throws an IllegalStateException and crashes the application because dialog is null.
What changes do you think we should make in order to solve the problem?
In patches/react-native/react-native+0.86.0+019+fix-modal-transparent-navigation-bar.patch, safeguard the deferred runnable by verifying that dialog still exists and is currently showing:
window.decorView.post {
val currentDialog = dialog
if (currentDialog !=null&& currentDialog.isShowing) {
updateSystemAppearance()
}
}
Re-apply and verify the patch with npx patch-package react-native.
Update patches/react-native/details.md with the null-safety rationale for patch 019.
What alternative solutions did you explore? (Optional)
Wrapping updateSystemAppearance() in a try-catch block: rejected because silently swallowing exceptions is defensive anti-pattern compared to explicit null/lifecycle checks.
Cancelling the runnable via removeCallbacks: adds unnecessary runnable reference tracking when a simple guard check achieves the exact same protection cleanly.
Please re-state the problem that we are trying to solve in this issue.
On Android (HybridApp, Report screen) the app crashes with IllegalStateException: dialog must exist when we call updateProperties, thrown from ReactModalHostView.updateSystemAppearance when it's called from the runnable posted in showOrUpdate (showOrUpdate$lambda$5).
What is the root cause of that problem?
Our patch react-native+0.86.0+019+fix-modal-transparent-navigation-bar.patch adds a deferred re-sync after newDialog.show(): window.decorView.post { updateSystemAppearance() }.
That runnable sits on the dialog window's decor view, not on the ReactModalHostView, so nothing cancels it when the modal goes away. It then reads the dialog field of the host view, which dismiss() sets to null. updateSystemAppearance() starts with checkNotNull(dialog), so it throws.
Upstream RN 0.86 dismiss() (ReactModalHostView.kt, lines 195-216) can null the field in two ways, and the second explains why this shows up on HybridApp:
Quick open → close. The modal is dismissed before the next frame (JS unmounts it, onDetachedFromWindow → onDropInstance, or a prop such as animationType sets createNewDialog and showOrUpdate dismisses and recreates it). The runnable is already queued on the main looper and runs afterwards.
The host activity is finishing.dismiss() deliberately skips nonNullDialog.dismiss() when the dialog's activity isFinishing, but still sets dialog = null. The dialog window stays attached until the activity's windows are torn down, so the posted runnable is guaranteed to run against a null field. HybridApp finishes the React Native activity while a modal can still be open (returning to the classic app), which fits Sentry's Android HybridApp + Report screen signature.
There's also a quieter variant of case 1. If the dialog was replaced rather than removed, the stale runnable from the old dialog runs updateSystemAppearance() on the new dialog before that dialog's own insets have settled. It doesn't crash, but it's the wrong dialog.
What changes do you think we should make in order to solve the problem?
Update patch 019 so the deferred re-sync is tied to the dialog it was scheduled for and is cancelled with it, instead of only null-checking:
In showOrUpdate, keep a reference to the runnable and to the dialog it was posted for. When it runs, only call updateSystemAppearance() if the host view's dialog is still that same dialog instance and it is showing; otherwise do nothing.
In dismiss(), remove the pending runnable from that dialog's decor view (removeCallbacks) before clearing dialog. Do this outside the isShowing branch, so it also covers the finishing-activity path where nonNullDialog.dismiss() is skipped.
Leave updateSystemAppearance()'s checkNotNull as it is, so any other caller with a genuinely missing dialog still fails loudly instead of being silently ignored.
Update the 019 entry in patches/react-native/details.md to describe the deferred sync and how it's cancelled.
Testing on Android: open and close a modal within one frame (rapid tap on a report action that opens a modal); open a modal on the Report screen and close the React Native activity from HybridApp; change a prop that recreates the dialog while it is open. None should crash, and the transparent navigation bar fix from #69004 / #87810 should still apply when a modal opens normally.
What alternative solutions did you explore? (Optional)
Only add dialog != null && dialog.isShowing in the runnable (the existing proposals). It stops this crash, but leaves the runnable alive after dismissal, and lets a stale runnable from a replaced dialog act on the new one.
Replace the post with a one-shot pre-draw listener on the dialog's decor view. It's cancelled automatically when the decor view detaches, but in the finishing-activity case the decor view doesn't detach, so the null case still needs the identity check above.
Activity
melvin-bot commentedon Oct 12, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @cretadn22 (
External)[-][Sentry: APP-M66] Android: ReactModalHostView updateSystemAppearance "dialog must exist"[/-][+][$175] [Sentry: APP-M66] Android: ReactModalHostView updateSystemAppearance "dialog must exist"[/+]melvin-bot commentedon Oct 12, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022109653602347544408
haiquywork commentedon Oct 12, 2026
Proposal
What is the root cause of that problem?
Our own React Native patch
react-native+0.86.0+019+fix-modal-transparent-navigation-bar.patch(added in #69004) addswindow.decorView.post { updateSystemAppearance() }insideReactModalHostView.showOrUpdate, right afternewDialog.show().App/patches/react-native/react-native+0.86.0+019+fix-modal-transparent-navigation-bar.patch
Lines 16 to 19 in 6502fec
The post runs on a later UI-thread tick. If the modal is dismissed in between (common with quick-closing Report screen modals),
dismiss()has already set thedialogfield to null.updateSystemAppearancethen does a checkNotNull ondialogand throws, which matches the trace (showOrUpdate$lambda$5 -> updateSystemAppearance). The unpatched code only called it synchronously right after show(), so it could not hit this.What changes do you think we should make in order to solve the problem?
window.decorView.post { val d = dialog if (d != null && d.isShowing) updateSystemAppearance() }window.decorView.isAttachedToWindow).patches/react-native/details.mdto mention the guard.What alternative solutions did you explore? (Optional)
updateSystemAppearanceitself return early whendialogis null: also fine, but it changes more upstream code than needed, and the posted lambda is the only caller that runs late.MelvinBot commentedon Oct 12, 2026
Proposal
What is the root cause of that problem?
Our react-native patch schedules a delayed
updateSystemAppearance()call that can run after the modal has closed.Fix: Navigation bar translucency on Android added this line to
ReactModalHostView.showOrUpdate()inreact-native+0.86.0+019+fix-modal-transparent-navigation-bar.patch:18:window.decorView.post { updateSystemAppearance() }The crash happens in this order:
showOrUpdate()posts the runnable to the dialog's decor view.dismiss()setsdialog = null.updateSystemAppearance()starts withcheckNotNull(dialog) { "dialog must exist when we call updateProperties" }, which throwsIllegalStateExceptionand crashes the app.Evidence
showOrUpdate$lambda$5 (ReactModalHostView.kt:348)matches line 348 of the patched file, which holds the posted call. The crash is in the lambda, not in the directupdateSystemAppearance()call one line above it.dismiss()setsdialog = nullat line 210, andupdateSystemAppearance()runscheckNotNull(dialog)at line 440. Nothing cancels the posted runnable.if (dialog.isShowing) updateSystemAppearance()inupdateProperties(), is safe because it runs synchronously whiledialogis non-null.What changes do you think we should make in order to solve the problem?
In patch
019, update the posted runnable so it callsupdateSystemAppearance()only ifdialogstill exists and is showing when the runnable runs. Otherwise, it does nothing. Make this a small change to the patch itself, and keep the patch header inpatches/react-native/details.mdaccurate.This keeps the navigation bar fix from the PR above and removes the crash.
What alternative solutions did you explore? (Optional)
updateSystemAppearance()return early whendialogis null, instead of usingcheckNotNull. This also works, but it changes upstream behavior for every caller and could hide real bugs.Runnableand callremoveCallbacksindismiss(). This is more precise but adds more code to the patch for the same result.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/103778)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
view run
mountiny commentedon Oct 12, 2026
(Vit's agent) @cretadn22, can you review the proposal from MelvinBot on this issue? Please tell us which of these fits:
jloa-dev commentedon Oct 13, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On Android, a Sentry crash (APP-M66) occurs with
java.lang.IllegalStateException: dialog must exist when we call updatePropertiesinsideReactModalHostView.updateSystemAppearance.What is the root cause of that problem?
In our React Native patch
patches/react-native/react-native+0.86.0+019+fix-modal-transparent-navigation-bar.patch#L18, we schedule a deferred UI tick to update system bar appearances:window.decorView.post { updateSystemAppearance() }When a modal is rapidly closed or unmounted immediately after opening,
dismiss()runs before the posted runnable executes on the looper queue, settingdialog = null. When the posted runnable subsequently runs,updateSystemAppearance()executes:checkNotNull(dialog) { "dialog must exist when we call updateProperties" }which throws an
IllegalStateExceptionand crashes the application becausedialogis null.What changes do you think we should make in order to solve the problem?
patches/react-native/react-native+0.86.0+019+fix-modal-transparent-navigation-bar.patch, safeguard the deferred runnable by verifying thatdialogstill exists and is currently showing:window.decorView.post { val currentDialog = dialog if (currentDialog != null && currentDialog.isShowing) { updateSystemAppearance() } }npx patch-package react-native.patches/react-native/details.mdwith the null-safety rationale for patch 019.What alternative solutions did you explore? (Optional)
updateSystemAppearance()in atry-catchblock: rejected because silently swallowing exceptions is defensive anti-pattern compared to explicit null/lifecycle checks.removeCallbacks: adds unnecessary runnable reference tracking when a simple guard check achieves the exact same protection cleanly.cretadn22 commentedon Oct 14, 2026
@mountiny We should ask for expert contributor help, because it needs deeper native
tnclaudecode commentedon Oct 17, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On Android (HybridApp, Report screen) the app crashes with
IllegalStateException: dialog must exist when we call updateProperties, thrown fromReactModalHostView.updateSystemAppearancewhen it's called from the runnable posted inshowOrUpdate(showOrUpdate$lambda$5).What is the root cause of that problem?
Our patch
react-native+0.86.0+019+fix-modal-transparent-navigation-bar.patchadds a deferred re-sync afternewDialog.show():window.decorView.post { updateSystemAppearance() }.That runnable sits on the dialog window's decor view, not on the
ReactModalHostView, so nothing cancels it when the modal goes away. It then reads thedialogfield of the host view, whichdismiss()sets tonull.updateSystemAppearance()starts withcheckNotNull(dialog), so it throws.Upstream RN 0.86
dismiss()(ReactModalHostView.kt, lines 195-216) can null the field in two ways, and the second explains why this shows up on HybridApp:onDetachedFromWindow→onDropInstance, or a prop such asanimationTypesetscreateNewDialogandshowOrUpdatedismisses and recreates it). The runnable is already queued on the main looper and runs afterwards.dismiss()deliberately skipsnonNullDialog.dismiss()when the dialog's activityisFinishing, but still setsdialog = null. The dialog window stays attached until the activity's windows are torn down, so the posted runnable is guaranteed to run against a null field. HybridApp finishes the React Native activity while a modal can still be open (returning to the classic app), which fits Sentry's Android HybridApp + Report screen signature.There's also a quieter variant of case 1. If the dialog was replaced rather than removed, the stale runnable from the old dialog runs
updateSystemAppearance()on the new dialog before that dialog's own insets have settled. It doesn't crash, but it's the wrong dialog.What changes do you think we should make in order to solve the problem?
Update patch 019 so the deferred re-sync is tied to the dialog it was scheduled for and is cancelled with it, instead of only null-checking:
showOrUpdate, keep a reference to the runnable and to the dialog it was posted for. When it runs, only callupdateSystemAppearance()if the host view'sdialogis still that same dialog instance and it is showing; otherwise do nothing.dismiss(), remove the pending runnable from that dialog's decor view (removeCallbacks) before clearingdialog. Do this outside theisShowingbranch, so it also covers the finishing-activity path wherenonNullDialog.dismiss()is skipped.updateSystemAppearance()'scheckNotNullas it is, so any other caller with a genuinely missing dialog still fails loudly instead of being silently ignored.patches/react-native/details.mdto describe the deferred sync and how it's cancelled.Testing on Android: open and close a modal within one frame (rapid tap on a report action that opens a modal); open a modal on the Report screen and close the React Native activity from HybridApp; change a prop that recreates the dialog while it is open. None should crash, and the transparent navigation bar fix from #69004 / #87810 should still apply when a modal opens normally.
What alternative solutions did you explore? (Optional)
dialog != null && dialog.isShowingin the runnable (the existing proposals). It stops this crash, but leaves the runnable alive after dismissal, and lets a stale runnable from a replaced dialog act on the new one.postwith a one-shot pre-draw listener on the dialog's decor view. It's cancelled automatically when the decor view detaches, but in the finishing-activity case the decor view doesn't detach, so the null case still needs the identity check above.