ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

🎯Comparing with : Understanding Their Use Cases and Differences in Android

Leo N
ProAndroidDev
Published in
8 min readNov 9, 2024

Photo by Simon Berger on Unsplash

When working with Kotlin coroutines in Android development, managing asynchronous tasks effectively is essential. Two coroutine scopes that Android developers frequently encounter are and . While both help manage coroutines, their behavior and use cases differ significantly. In this blog post, we'll explore these differences, focusing on how they handle exceptions, manage child coroutines, and why choosing the right scope for each task is crucial for building efficient, error-resilient applications.

Purpose and Use Cases

SupervisorScope

  • is designed for managing independent coroutines that should not affect each other if one of them fails. This scope is useful in scenarios where you want to run multiple concurrent tasks but don’t want the failure of one tasks to cancel all others. For instance, if your application needs to make multiple network requests to different APIs, and a failure in one request should not affect the others, is the right choice.
supervisorScope {
launch {
// Task 1
throw Exception("This will only cancel Task 1")
}
launch {
// Task 2
}
// Task 3
}

// Output
Task 1
... Exception
Task 2
Task 3
  • A common use of is when managing concurrent operations that are loosely related, such as performing different network or data-fetching operations in parallel without each task being dependent on the success of the others.

viewModelScope

  • The is tied to the view model such that when the view model is cleaned up (i.e. is called), the scope is cancelled. This ensures that when your view model goes away, so does all the coroutine work associated with it. This avoids wasted work and memory leaks.
viewModelScope.launch {
// Fetch data for the UI
}
  • The uses the coroutine dispatcher. A controls how a coroutine runs, including what thread the coroutine code runs on. puts the coroutine on the UI or main thread. This makes sense as a default for coroutines, because often, view models manipulate the UI.

if your UI is dependent on data-fetching or complex calculations, using ensures that these tasks are automatically stopped when no longer needed, helping conserve resources and prevent memory leaks.

Cancellation behavior

  • Within a , if one child coroutine throws an exception, it does not cancel the other sibling coroutines. This is different from a regular coroutine scope, where a failure in one child would cancel all siblings. This behavior is useful for operations that should remain unaffected by the failure of other tasks.
  • However, if the itself is canceled, all child coroutines are canceled regardless of individual status. Because all sibling coroutines are independent of each other. If one coroutine fails, the failure does not propagate to its siblings.
  • It is designed to avoid the default failure propagation behavior, which is why it’s often used for tasks that should run independently, even if one of them encounters an error.

  • In , cancellation is tied to the lifecycle. If the is cleared (e.g., when the user navigates away from a screen), all coroutines in this scope are canceled automatically. This ensures that ongoing work does not continue if the related UI is no longer present.
  • is indeed part of a structured concurrency model, but its foundation on modifies its behavior. In typical structured concurrency (like with a regular ), an exception in any child coroutine would cancel all other sibling coroutines.
public val ViewModel.viewModelScope: CoroutineScope
get() {
...
return setTagIfAbsent(
JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main)
)
}
...
}
  • The here is set up with a and a dispatcher, but it lacks a . (Read on to learn what CoroutineExceptionHandler is). By default, any unhandled exceptions will result in a stack trace being printed, which is then followed by the app crashing. To ensure better handling of uncaught exceptions and to monitor errors in production, it's advisable to define a custom instead of relying solely on the helper.

Implementation

  • is created using the function in Kotlin. It does not belong to any lifecycle component by default, so it’s often used inside custom coroutine builders or with lifecycle observers.
fun main() = runBlocking {
launch {
supervisorScope {
// Launch Task 1 inside supervisorScope
launch {
try {
println("Task 1 started")
// Simulate some work
for (i in 1..5) {
println("Task 1 is working... $i")
delay(500)
}
println("Task 1 completed")
} catch (e: Exception) {
println("Task 1 failed with exception: ${e.message}")
}
}

// Launch Task 2 inside supervisorScope (this one will throw an exception)
launch {
try {
println("Task 2 started")
// Simulate some work that will fail
delay(1000)
throw Exception("Simulated error in Task 2")
} catch (e: Exception) {
println("Task 2 failed with exception: ${e.message}")
}
}

// Launch Task 3 inside supervisorScope
launch {
try {
println("Task 3 started")
// Simulate some work
for (i in 1..5) {
println("Task 3 is working... $i")
delay(700)
}
println("Task 3 completed")
} catch (e: Exception) {
println("Task 3 failed with exception: ${e.message}")
}
}
}
}
}

  • is automatically available within any that imports the library. You simply call to start a coroutine in this scope.
  • When working with , if you need to prevent one coroutine’s failure from canceling others, you would need to handle errors explicitly. This can be done by using blocks within each coroutine or by using to manage failures gracefully.

Let see this example:

  • Task 1 and Task 3 perform some work in a loop, with delays to simulate ongoing tasks.
  • Task 2 deliberately throws an exception after a delay, simulating a failure.
  • Each block in has its own block to handle exceptions locally.
class MyViewModel : ViewModel() {

fun startTasks() {
// Launch Task 1 in viewModelScope
viewModelScope.launch {
try {
println("Task 1 started")
// Simulate some work
for (i in 1..5) {
println("Task 1 is working... $i")
kotlinx.coroutines.delay(500)
}
println("Task 1 completed")
} catch (e: Exception) {
println("Task 1 failed with exception: ${e.message}")
}
}

// Launch Task 2 in viewModelScope (this one will throw an exception)
viewModelScope.launch {
try {
println("Task 2 started")
// Simulate some work that will fail
kotlinx.coroutines.delay(1000)
throw Exception("Simulated error in Task 2")
} catch (e: Exception) {
println("Task 2 failed with exception: ${e.message}")
}
}

// Launch Task 3 in viewModelScope
viewModelScope.launch {
try {
println("Task 3 started")
// Simulate some work
for (i in 1..5) {
println("Task 3 is working... $i")
kotlinx.coroutines.delay(700)
}
println("Task 3 completed")
} catch (e: Exception) {
println("Task 3 failed with exception: ${e.message}")
}
}
}
}

// Output
Task 1 started
Task 3 started
Task 2 started
Task 1 is working... 1
Task 3 is working... 1
Task 1 is working... 2
Task 2 failed with exception: Simulated error in Task 2
Task 3 is working... 2
Task 1 is working... 3
... and so on ...
Task 1 completed
Task 3 completed

Task 2’s failure doesn’t affect Task 1 or Task 3 — they continue working as expected. This approach is ideal for scenarios where tasks should fail independently without disrupting other ongoing tasks, such as multiple network requests in a UI component.

UncaughtExceptionHandler

Let’s talk about , yes is relevant to how coroutine exceptions are handled, especially in coroutine scopes like or . Here’s how it ties in:

UncaughtExceptionHandler Basics

  • In Java and Kotlin, is a handler that catches unhandled exceptions in threads. In coroutines, unhandled exceptions are managed differently depending on the coroutine’s context, scope, and parent-child relationships.

Uncaught Exceptions in

  • uses structured concurrency, where all coroutines in the scope are linked to the . If an exception is thrown in one coroutine and it’s uncaught (not managed by a ), the exception will propagate to the scope’s parent, which is itself. This means all sibling coroutines will be canceled by default, unless the exception is handled explicitly within each coroutine.
  • If an exception is not caught, it bubbles up to the default . However, since is lifecycle-aware, uncaught exceptions lead to a cleanup of the scope by canceling all coroutines, which makes an explicit particularly useful if you want to handle failures without propagating cancellation.

SupervisorScope and Exception Handling

  • allows coroutines to run independently. If one coroutine within throws an uncaught exception, it doesn’t automatically cancel its siblings. This behavior is controlled by , which essentially overrides the usual behavior of cascading cancellation within a structured concurrency model.
  • With , if you add an (through ), you can manage exceptions without affecting other sibling coroutines, making it useful for tasks where partial failures are expected.

Using CoroutineExceptionHandler with viewModelScope and SupervisorScope

  • can be attached to individual coroutines in both and to manage uncaught exceptions gracefully. For example, you can log errors or perform specific actions when a coroutine fails without canceling the entire scope.
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.e("Error", "Caught exception: ${throwable.message}")
}

viewModelScope.launch(exceptionHandler) {
// Coroutine with exception handler
throw Exception("Test exception")
}

In this case, if an exception is thrown, it will be caught by the rather than propagating up and canceling other coroutines in .

(via ) plays a crucial role in managing unhandled exceptions in coroutines. In , unhandled exceptions typically cancel the entire scope, but by adding a , you can manage errors without canceling all coroutines. In , this isolation is even more direct, as sibling coroutines are designed not to affect each other’s execution upon failure.

Summary

In summary, provides independent failure handling, making it useful for concurrent operations where failure in one shouldn’t impact others. , on the other hand, is ideal for managing coroutines tied to the lifecycle of a , especially for tasks related to UI that need to stop when the screen is no longer in use.

There are a few points to clarify regarding this:

SupervisorJob

A failure or cancellation of a child does not cause the supervisor job to fail and does not affect its other children, so a supervisor can implement a custom policy for handling failures of its children. If a parent job is specified, then this supervisor job becomes a child job of parent and is cancelled when the parent fails or is cancelled. All this supervisor’s children are cancelled in this case, too.

ViewModelScope is defined with , why does it still failed?

  • In Kotlin coroutines, when you create a , it usually isolates the failures of its child coroutines—meaning if one child fails, it doesn’t affect the other children within the same . However, if you specify a parent job for this , things work a bit differently because it now inherits the parent’s lifecycle and failure handling.
  • When the parent job fails or is cancelled, all of its child jobs — including the —are cancelled. This cancellation is propagated downwards to all children in the job hierarchy, meaning that even the children of the will be cancelled.

References

Here are some insightful articles that explore the differences between and , particularly in terms of exception handling, structured concurrency, and testing:

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Written by Leo N

🇻🇳 🇸🇬 🇲🇾 🇦🇺 🇹🇭 Engineer @ GXS Bank, Singapore | MSc 🎓 | Technical Writer . https://github.com/nphausg

Responses (3)

Write a response

I've checked source code of viewModelScope and it uses the same SupervisorJob like supervisorScope. Exception propagation isn't happening for viewModelScope. Are you sure about viewModelScope behaviour ?

--

I'm a bit confused, so in both supervisor and viewmodel if one task fails they scope is still working so both is good for concurrent tasks?

--