kotlinx.coroutines: IllegalStateException : Already resumed

Hi,

I am observing live data using observeForever() and then removing the observer later. I handle success and failure scenarios and resume the continuation object accordingly.

//failure removeObserver(observer) if (coroutine.isActive) coroutine.resume(getErrorState()) //success removeObserver(observer) if (coroutine.isActive) coroutine.resume(data)

In most cases it works fine. However, randomly few times I get exception :

Fatal Exception: java.lang.IllegalStateException: Already resumed, but proposed with update android.widget.RemoteViews@ebbd63d at kotlinx.coroutines.CancellableContinuationImpl.alreadyResumedError(CancellableContinuationImpl.java:277) at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.java:272) at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.java:189)

It seems , the isActive check isn’t behaving as expected. Please suggest a better check to avoid this crash.

Cheers !

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 1
  • Comments: 18 (6 by maintainers)

Most upvoted comments

Here are my 2 cents, I had the same issue when I wrapped an API callback into a suspendCancellableCoroutine, the callback inside was receiving calls in different methods, therefore cont.resume was being called twice, what I ended up doing was an extension function to only call resume if the cont was active, like:

private fun <T> CancellableContinuation<T>.resumeIfActive(value: T) {
    if(isActive) {
        resume(value)
    }
}

for my case, this works since I only care about the first call to cont.resume

@aruke a strategy I used to use was to save the continuation in a nullable var, resuming from the var and immediately setting it to null afterwards. Then, if other callbacks eventually try to resume it again, field will be null.

But honestly, it works but it’s flaky design. In my opinion you should only suspend and resume if there’s a single callback that is guaranteed to always run and run exactly once. For example, GMS tasks and addOnCompleteListener.

If you have a code snippet to share I could give further input.

Calling resume, usually, just schedules coroutine for execution in the corresponding coroutine dispatcher.

It looks like you call resume twice on the same continuation.

@elizarov Can you please look into this issue again? We have a simpler use-case where the issue is reproduced, hopefully it will be useful for you:


import android.app.Activity
import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.google.android.play.core.review.ReviewManager
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
import javax.inject.Inject
import kotlin.coroutines.resume

/**
 * An interface to display a UI requesting user feedback
 */
interface FeedbackRequestUiPresenter {

    /**
     * Show Feedback Request UI to the user
     *
     * @return feedback option selected by the user
     */
    suspend fun showFeedbackRequest(): UserFeedbackResult
}

/**
 * Class that requests a user feedback using a dialog created by [UserFeedbackDialogBuilder].
 */
class FeedbackRequestDialogPresenter @Inject constructor(
    private val fragment: Fragment,
    private val lifecycleOwner: LifecycleOwner,
    private val reviewManager: ReviewManager
) : FeedbackRequestUiPresenter {

    override suspend fun showFeedbackRequest(): UserFeedbackResult =
        suspendCancellableCoroutine { continuation ->
            lifecycleOwner.lifecycleScope.launchWhenResumed {
                showDialog(continuation)
            }
        }

    private fun showDialog(continuation: CancellableContinuation<UserFeedbackResult>) {
        val activity = fragment.requireActivity()
        UserFeedbackDialogBuilder(activity).apply {

            setOnDismissListener {
                continuation.resume(UserFeedbackResult.DISMISS)
            }

            setOnSubmitReviewClickListener {
                requestRating(activity) {
                    continuation.resume(UserFeedbackResult.LEAVE_REVIEW)
                }
            }

            setOnContactSupportClickListener {
                continuation.resume(UserFeedbackResult.CONTACT_SUPPORT)
            }

            setOnAskLaterClickListener {
                continuation.resume(UserFeedbackResult.ASK_LATER)
            }
        }.show()
    }

    private fun requestRating(activity: Activity, onCompleteBlock: () -> Unit) {
        reviewManager
            .requestReviewFlow()
            .addOnCompleteListener { reviewInfo ->
                if (reviewInfo.isSuccessful) {
                    reviewManager
                        .launchReviewFlow(activity, reviewInfo.result)
                        .addOnCompleteListener { onCompleteBlock() }
                }
            }
    }
}


class UserFeedbackDialogBuilder @JvmOverloads constructor(context: Context, themeResId: Int = 0) :
    MaterialAlertDialogBuilder(context, themeResId) {

    private val binding = FeedbackDialogBinding.inflate(context.inflater)

    init {
        setView(binding.root)
    }

    var onDismissListener: DialogInterface.OnDismissListener? = null
        private set

    override fun setOnDismissListener(
        onDismissListener: DialogInterface.OnDismissListener?
    ): MaterialAlertDialogBuilder {
        this.onDismissListener = onDismissListener
        return super.setOnDismissListener(onDismissListener)
    }

    fun setOnSubmitReviewClickListener(listener: (View) -> Unit) =
        binding.submitReview.setOnClickListener(listener)

    fun setOnContactSupportClickListener(listener: (View) -> Unit) =
        binding.contactSupport.setOnClickListener(listener)

    fun setOnAskLaterClickListener(listener: (View) -> Unit) =
        binding.askLater.setOnClickListener(listener)
}

The exception stacktrace:

Fatal Exception: java.lang.IllegalStateException
Already resumed, but proposed with update CONTACT_SUPPORT

kotlinx.coroutines.CancellableContinuationImpl.alreadyResumedError (CancellableContinuationImpl.kt:335)
kotlinx.coroutines.CancellableContinuationImpl.resumeImpl (CancellableContinuationImpl.kt:330)
kotlinx.coroutines.CancellableContinuationImpl.resumeWith (CancellableContinuationImpl.kt:250)
app.sample.feedback.FeedbackRequestDialogPresenter$showDialog$$inlined$apply$lambda$3.invoke (FeedbackRequestUiPresenter.kt:57)
app.sample.feedback.FeedbackRequestDialogPresenter$showDialog$$inlined$apply$lambda$3.invoke (FeedbackRequestUiPresenter.kt:29)
app.sample.feedback.UserFeedbackDialogBuilder$sam$android_view_View_OnClickListener$0.onClick (Unknown Source:2)
android.view.View.performClick (View.java:7192)
com.google.android.material.button.MaterialButton.performClick (MaterialButton.java:992)

@elizarov I’m having a similar issue in my app. My question is why coroutine doesn’t resume immediately after Continuation.resume gets called? That way we’d never get this sort of exception (calling resume on an already resumed Continuation. I’m certain there is some reasons behind. Interested to hear your guidance.

It just means that you called resume on the same continuation twice. It is hard to figure out how this might happen just by a snippet of your code.