TL;DR
You called FragmentManager.commit() after Android already saved the Activity's state โ usually because an async callback fired after the user pressed Home or rotated the screen. Two ways out: swap commit() for commitAllowingStateLoss() when losing the transaction on restore is fine, or guard the commit with a lifecycle check before it runs.
Full Error
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at androidx.fragment.app.FragmentManager.checkStateLoss(FragmentManager.java:...)
at androidx.fragment.app.FragmentManager.enqueueAction(FragmentManager.java:...)
at androidx.fragment.app.BackStackRecord.commitInternal(BackStackRecord.java:...)
at androidx.fragment.app.BackStackRecord.commit(BackStackRecord.java:...)
Why This Happens
When Android calls onSaveInstanceState(), it snapshots your Activity โ including the entire Fragment back stack. After that snapshot is taken, FragmentManager refuses any further changes to the back stack. Touch it anyway and you get this crash.
Where it usually strikes:
- A Retrofit or coroutine callback returns after the user pressed Home or the screen rotated
- A dialog committed inside a
DialogInterface.OnClickListener - Fragment commits inside
onActivityResult(), which runs beforeonResume() - A background thread posting to the main thread after the Activity is already paused
- A
LiveDataobserver or RxJava stream emitting after the lifecycle went inactive
Fix 1: commitAllowingStateLoss() โ Quick and Dirty
Swap commit() for commitAllowingStateLoss(). It skips the state-loss check entirely. The trade-off: if the Activity gets recreated, this transaction won't replay. That's acceptable for throwaway UI like a loading spinner โ not for actual navigation.
// Before
supportFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment())
.commit()
// After
supportFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment())
.commitAllowingStateLoss()
Java version:
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, new MyFragment())
.commitAllowingStateLoss();
Stick to this only for truly disposable transactions โ hiding a progress bar, dismissing a loading dialog. Navigation and anything the user should see after a config change needs a proper fix.
Fix 2: Check Lifecycle State Before Committing
Guard the commit so it only runs when the Activity is in a safe state.
// Kotlin โ in an Activity
if (!isFinishing && !isDestroyed) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment())
.commit()
}
// Kotlin โ in a Fragment
if (isAdded && !requireActivity().isFinishing) {
parentFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment())
.commit()
}
For coroutine-based code, lifecycle.withStarted is cleaner. It suspends until the lifecycle reaches at least STARTED โ meaning onStart() has run โ then executes the block.
lifecycleScope.launch {
lifecycle.withStarted {
supportFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment())
.commit()
}
}
Requires lifecycle-runtime-ktx 2.4.0+. If the lifecycle never reaches STARTED again (e.g. the Activity finishes), the block simply never runs.
Fix 3: Use LiveData with the Right Lifecycle Owner
Crashing inside a ViewModel observer? Check which lifecycle owner you're passing. In a Fragment, this refers to the Fragment itself โ it outlives the view. Use viewLifecycleOwner instead so the observer dies with the view.
// Wrong โ observer stays alive even after the view is destroyed
viewModel.result.observe(this) { navigateToDetail(it) }
// Correct โ tied to the Fragment's view lifecycle
viewModel.result.observe(viewLifecycleOwner) { navigateToDetail(it) }
With viewLifecycleOwner, the observer is removed automatically when the Fragment goes off-screen. No more stale callbacks firing into a dead Fragment.
Fix 4: onActivityResult() Timing Problem
onActivityResult() fires before onResume(), so onSaveInstanceState() has already run at that point. Committing a Fragment there will crash. Store the action instead and run it once you're back in onResume().
private var pendingAction: (() -> Unit)? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == MY_REQUEST && resultCode == RESULT_OK) {
pendingAction = { showResultFragment() }
}
}
override fun onResume() {
super.onResume()
pendingAction?.invoke()
pendingAction = null
}
On API 29+ (or with the Activity Result API via registerForActivityResult), this workaround isn't needed โ the callback already fires after onResume().
Fix 5: Retrofit and Async Callbacks
Network calls are the number-one source of this crash in production. A request takes 2โ3 seconds; the user swipes away after 1. The callback lands, tries to commit a Fragment, and boom.
// Bad โ commits with no lifecycle awareness
retrofitService.getData().enqueue(object : Callback<Data> {
override fun onResponse(call: Call<Data>, response: Response<Data>) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, ResultFragment.newInstance(response.body()!!))
.commit()
}
...
})
// Better โ cancel the call when the screen goes background
override fun onStop() {
super.onStop()
pendingCall?.cancel()
}
The real long-term fix is moving to coroutines with viewModelScope. The scope cancels automatically when the ViewModel clears, so the fragment transaction never even gets scheduled.
viewModelScope.launch {
val result = repository.getData() // auto-cancelled if ViewModel is cleared
_uiState.value = result
}
Pair this with a StateFlow or LiveData observed via viewLifecycleOwner and you've eliminated the whole problem class.
Which Fix to Use?
- Async callback (Retrofit, RxJava) โ Migrate to coroutines with
viewModelScope+LiveData/StateFlow - LiveData observer firing too late โ Switch to
viewLifecycleOwnerin your Fragment - onActivityResult timing โ Pending action pattern, or just migrate to the Activity Result API
- Loading/progress dialog โ
commitAllowingStateLoss()works fine here - Need a fast band-aid โ Lifecycle guard (
isAdded && !isFinishing) +commitAllowingStateLoss()
Verifying the Fix
- Reproduce the crash: trigger the async action, then immediately press Home or rotate before it finishes.
- Apply the fix and repeat the same steps.
- Check Logcat โ zero
IllegalStateExceptionlines should appear. - Confirm the Fragment transaction either committed correctly on resume or was skipped cleanly, depending on your approach.
- Run a stress test with rapid navigation and background network load:
adb logcat | grep IllegalStateExceptionto catch any edge cases you missed.
Further Reading
- AndroidX Fragment โ
FragmentManagerdocumentation - Android Lifecycle-aware components guide
lifecycleScopeandviewModelScopeโ Kotlin coroutines in Android- Activity Result API โ replacement for
startActivityForResult

