Demystify coroutineScope (not CoroutineScope)
coroutineScope, supervisorScope, withContext ...
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())
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()
}
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)
}
}
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)
}
}
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)
}
}
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
}
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)
}
}
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)
}
}
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:
Happy Coding ✌️