Fix IllegalStateException: Can not perform this action after onSaveInstanceState in Android Fragment

intermediate๐Ÿ“ฑ Android2026-05-03| Android (API 11+), Java/Kotlin, AndroidX Fragment 1.x, affects Activities and Fragments using FragmentManager

Error Message

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
#fragment#lifecycle#fragment-transaction#android-ui

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 before onResume()
  • A background thread posting to the main thread after the Activity is already paused
  • A LiveData observer 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 viewLifecycleOwner in 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 IllegalStateException lines 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 IllegalStateException to catch any edge cases you missed.

Further Reading

  • AndroidX Fragment โ€” FragmentManager documentation
  • Android Lifecycle-aware components guide
  • lifecycleScope and viewModelScope โ€” Kotlin coroutines in Android
  • Activity Result API โ€” replacement for startActivityForResult

Related Error Notes