DEV Community

Cover image for Demystify coroutineScope (not CoroutineScope)
Khush Panchal
Khush Panchal

Posted on • Originally published at Medium

Demystify coroutineScope (not CoroutineScope)

In this article we will deep dive into coroutineScope (it’s small c)

  • Difference between coroutineScope and CoroutineScope?
  • Difference between coroutineScope and supervisorScope?
  • Difference between coroutineScope and withContext?

Terminologies

Before diving into the topic, we need to know some basic terminologies:

Coroutine: Framework to manage concurrency, written on top of the actual threading framework. In simplest term, piece of code that can be suspended and resumed without blocking the thread.

Scope: A scope in coroutines defines the lifetime and context of a coroutine. Each coroutine runs inside some scope.

CoroutineBuilder: Functions responsible for creating and starting a new coroutine. e.g., launch{}, async{}, runBlocking{}

Suspend Function: Function that can suspend and resume the code execution without blocking the thread. It can be call from suspend function or coroutine block only.

CoroutineContext: Provide contextual information(CoroutineDispatcher, Job, CoroutineExceptionHandler) for the coroutine. It also tells coroutine on which thread it need to run.

CoroutineExceptionHandler: Part of CoroutineContext to handle uncaught exceptions. Should be used in the root coroutine to handle all the child coroutine uncaught exceptions.

coroutineScope vs CoroutineScope

CoroutineScope

CoroutineScope is just an interface that have coroutineContext object. Each coroutine required some scope to run.

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}
// Creates CoroutineScope and wraps the given context
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())
Enter fullscreen mode Exit fullscreen mode

CoroutineScope constructor need CoroutineContext which then combines with the coroutine builder to start a new coroutine.

Say we want to perform some background task, we can simply do:

CoroutineScope(Dispatchers.IO).launch {
  doSomeBackgroundTask()
}
Enter fullscreen mode Exit fullscreen mode

It is straightforward, but what if we want to make few parallel api calls, we can do like this:

CoroutineScope(Dispatchers.IO).launch {
    try {
        launch { postSchoolData() }
        launch { postTeacherData() }
        launch { postStudentData() }
    } catch (e: Exception) {
        log("Catch exception: " + e.message)
    }
}
Enter fullscreen mode Exit fullscreen mode

Great, it looks good, but there is one catch, what if one api call failed (say postTeacherData()), what will happen?

App will crash, yes even writing inside the try-catch block, app will crash.

Reason behind this is how coroutines handles the error. When there is an exception in normal function it just re-throws the exception, but that is not the case with coroutines.

In coroutines if there is an exception occurs inside child coroutine, it propagates to the parent. In our example, if error occurs in inner launch, it will propagate to the outer launch block inspite of having try-catch block.

Since in our outer launch block, we are not passing any exception handler, our app is crashing. So one solution is using exception handler, but there is one more better way to do it — coroutineScope

coroutineScope

public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = ScopeCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}
Enter fullscreen mode Exit fullscreen mode

Looking at the source code, it can be seen coroutineScope is just a suspend function that creates a sub-scope in the coroutine hierarchy.

  • It takes the caller’s CoroutineContext and unlike normal launch it does not propagate exceptions from its children but re-throws them instead.
  • It cancels all other children if one of them fails
  • It blocks the current coroutine until all of its child coroutines are completed.

coroutineScope{} re-throws exceptions of its failing children instead of propagating them up the job hierarchy.

CoroutineScope(Dispatchers.IO).launch {
    try {
        coroutineScope {
            launch { postSchoolData() }
            launch { postTeacherData(true) } //this throws exception
            launch { postStudentData() }
        }
    } catch (e: Exception) {
        log("Catch exception: " + e.message)
    }
}
Enter fullscreen mode Exit fullscreen mode

So in the above example, if the second child coroutine (postTeacherData()) throws an exception, rather than propagating to the parent coroutine, coroutineScope will re-throw the exception and when exception is re-thrown, it will catch inside the try-catch block.

But there is one drawback, in case there are multiple child of coroutineScope, failure of one child leads to cancellation of all other children.

Solution? — supervisorScope

coroutineScope vs supervisorScope

public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = SupervisorCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}

private class SupervisorCoroutine<in T>(
    context: CoroutineContext,
    uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
    override fun childCancelled(cause: Throwable): Boolean = false
}
Enter fullscreen mode Exit fullscreen mode

supervisorScope creates a new independent nested sub-scope. It doesn’t re-throws exceptions neither it propagate exceptions to the parent coroutine scope. It is similar to say using CoroutineScope with SupervisorJob (Job whose children can fail independently).

  • If one of the coroutines inside this scope fails, the others are not cancelled
  • Coroutines created inside supervisorScope become top-level Coroutines (we can add a CoroutineExceptionHandler in them)

In order for a CoroutineExceptionHandler to have an effect, it must be installed either in the CoroutineScope or in a top-level coroutine.

In case of supervisorScope, as parent is not cancelled, way of handling error is different than coroutineScope. supervisorScope checks for exception handlers and propagate it to the handler without cancelling, but if it does not have any exception handler, it will crash the app.

CoroutineScope(Dispatchers.IO).launch {
    try {
        supervisorScope {
            launch { postSchoolData() }
            launch { postTeacherData(true) } //this throws exception
            launch { postStudentData() }
        }
    } catch (e: Exception) {
        log("Catch exception: " + e.message)
    }
}
Enter fullscreen mode Exit fullscreen mode

The above example will crash the application as supervisorScope creates top-level coroutine and we are not passing exception handler. But if we use async instead, it will hold the error, and if we call await() without try-catch it will fail the supervisorScope and supervisorScope will re-throw the error.

CoroutineScope(Dispatchers.IO).launch {
    try {
        supervisorScope {
            val totalStudent = async { getStudentCount(true) } //this throws exception
            val totalTeacher = async { getTeacherCount() }
            try {
                totalStudent.await()
            } catch (e: Exception) {
                log("Await Exception: " + e.message)
            }
            try {
                totalTeacher.await()
            } catch (e: Exception) {
                log("Await Exception: " + e.message)
            }
        }
    } catch (e: Exception) {
        log("Catch exception: " + e.message)
    }
}
Enter fullscreen mode Exit fullscreen mode

The above example will work perfectly well without any child failure or exception but if in the above example we do not write totalStudent.await() inside try-catch block, it will fail the supervisorScope and error will be re-thrown by the supervisorScope which will get catch by outer try-catch block.

coroutineScope vs withContext

There is also one more suspend function withContext(context) {} which is used generally to switch context between running coroutines and it will only return when all it’s children completes.

coroutineScope ≡ withContext(this.coroutineContext)

In simple terms coroutineScope is equivalent to passing same parent context in withContext.

Source Code: Github

Contact Me:

LinkedIn, Twitter

Happy Coding ✌️

Top comments (1)

Collapse
 
kapaseker profile image
PangoSea

Gotcha