[Coroutines] Ambiguous coroutineContext issue when having suspended function with launch inside class that extends CoroutineScope

I have a base UseCase class:

abstract class UseCase<RESULT> internal constructor(private val executionDispatcher: CoroutineDispatcher,
                                                    private val postExecutionDispatcher: CoroutineDispatcher)
    : CoroutineScope {

    private val job: Job = Job()
    override val coroutineContext: CoroutineContext
        get() = executionDispatcher + job

    fun dispose() {
        job.cancel()
    }

    //---

    suspend operator fun invoke(): RESULT {
        return suspendCoroutine { continuation ->
            launch(executionDispatcher) {
                try {
                    val result = run()
                    launch(postExecutionDispatcher) {
                        continuation.resume(result)
                    }
                } catch (e: Exception) {
                    launch(postExecutionDispatcher) {
                        continuation.resumeWithException(e)
                    }
                }
            }
        }
    }


    internal abstract fun run(): RESULT
}

that:

  • uses suspended function invoke() and executes run() on executionDispatcher but return/throw on postExecutionDispatcher
  • extends CoroutineScope so therefore has possibility to be cancelled

When running code inspection on this class I get (on invoke()):

Ambiguous coroutineContext due to CoroutineScope receiver of suspend function

Detailed explanation:
This inspection reports calls & accesses of CoroutineScope extensions or members inside suspend functions with CoroutineScope receiver. Both suspend functions and CoroutineScope members & extensions have access to coroutineContext. When some function is simultaneously suspend and has CoroutineScope receiver, it has ambiguous access to CoroutineContext: first via kotlin.coroutines.coroutineContext and second via CoroutineScope.coroutineContext, and two these contexts are different in general case. So when we call some CoroutineScope extension or access coroutineContext from such a function, it’s unclear which from these two context do we have in mind. Normal ways to fix this are to wrap suspicious call inside coroutineScope { … } or to get rid of CoroutineScope function receive

The issue is caused by launch() methods inside my suspened function.
Is there a way to write that function in a different way but still meet requirements for UseCase class?

I’d go with something more like this:

abstract class UseCase<RESULT> internal constructor(private val executionDispatcher: CoroutineDispatcher) {
    suspend operator fun invoke(): RESULT = withContext(executionDispatcher) { run() }
    internal abstract suspend fun run(): RESULT
}

When withContext returns, it returns to the caller’s context and its dispatcher so there’s no need to manage a postExecutionDispatcher like with RxJava.

Since invoke is a suspend method, it will be running in the context of an already existing coroutine (which has lifetime management with its own Job) and that suggests to me that UseCase should perhaps not have its own lifetime. Do you really want to cancel UseCase itself instead of the Jobs using UseCase?

Also if run is not a suspend method then it loses access to the coroutine that it is running in so the benefits of structured concurrency are lost for that coroutine inside run. See: https://kotlinlang.org/docs/reference/coroutines/basics.html#structured-concurrency

Thank you for the solution and detailed explanation.