DEV Community

João Victor Martins
João Victor Martins

Posted on

Scopes in Coroutines - coroutineScope x supervisorScope

As a Kotlin developer, coroutines are an essential tool for writing efficient and performant code. When you know how to work with this tool, you can enhance your app's performance with simpler code. Writing code with coroutines is less complex than using other tools for asynchronous development, but, like any technology, you need to be aware of best practices to avoid potential issues. There are several key concepts you should understand when working with coroutines, including builders, scopes, contexts, and flows. In this post, I aim to explain the differences between coroutineScope and supervisorScope, starting with an introduction to scopes in coroutines.

Scopes

According to Kotlin's documentation, CoroutineScope defines a scope for new coroutines. Every coroutine builder (such as launch, async, etc.) is an extension of CoroutineScope and inherits its coroutineContext, automatically propagating all its elements and cancellation.

In simple terms, a scope is required to create coroutines and execute tasks. You can create scopes in several ways, one of which is using the runBlocking builder.

Image description

As you can see, the IDE shows a CoroutineScope after the curly brace. Inside this scope, you can create multiple coroutines. runBlocking is the easiest way to bridge from the non-coroutine world to the coroutine world, but be cautious when using it in production. As its name suggests, it is a blocking operation, which means it will block the main thread. If you remove runBlocking, the code won't compile because the launch builder won't be within a scope.

There are two other common ways to create scopes: using supervisorScope and coroutineScope. Both are the main subjects of this post, and I will explain each one in the following sections.

coroutineScope

coroutineScope is a function, and according to Kotlin's documentation, it is designed for concurrent decomposition of work. If any child coroutine in this scope fails, the scope fails, cancelling all other children. This function returns as soon as the given block and all its child coroutines are completed. An example of using a scope looks like this:

suspend fun main() {
    call()
}

suspend fun call() {
    coroutineScope {
        launch { executeOneDay(3) }
        launch { executeOneDay(1) }
    }
}

suspend fun executeOneDay(int: Int) {
    println("entrou")
    val numberOfItems = if (int == 1) {
        delay(1000)
        error("some error")
    } else {
        delay(2000)
        int
    }
    runCatching { teste(numberOfItems) }.onFailure { println(it.message) }
}

fun teste(int: Int) {
    println("chegou no teste $int")
}
Enter fullscreen mode Exit fullscreen mode

The first difference to note is the suspend keyword in the method signature. The function must be suspend to create a scope with coroutineScope. Unlike runBlocking, it does not block execution; instead, it utilizes suspend and resume activities. Let’s break down what this code does: The main() function calls the call() function. Inside call(), a suspend function, I create two coroutines using the launch builder within coroutineScope. Each coroutine calls the executeOneDay(int: Int) function with different parameters: 3 and 1.

In executeOneDay, if the parameter is 1, the code delays for 1 second and generates an error; if it’s anything other than 1, it delays for 2 seconds and returns the number. Since both coroutines run asynchronously, one will generate an error while the other will not. When you execute this code, you will see "entrou" printed twice in your terminal, followed by the error message, terminating the program. This occurs because, when one coroutine fails, coroutineScope cancels all other coroutines. This behavior is expected, but if you want different behavior, you can use supervisorScope.

supervisorScope

According to Kotlin's documentation, with supervisorScope, a failure of a child coroutine does not cause the scope to fail and does not affect other children. If we modify the previous code to use supervisorScope, the result will change:

suspend fun main() {
    call()
}

suspend fun call() {
    supervisorScope {
        launch { executeOneDay(3) }
        launch { executeOneDay(1) }
    }
}

suspend fun executeOneDay(int: Int) {
    println("entrou")
    val numberOfItems = if (int == 1) {
        delay(1000)
        error("some error")
    } else {
        delay(2000)
        int
    }
    runCatching { teste(numberOfItems) }.onFailure { println(it.message) }
}

fun teste(int: Int) {
    println("chegou no teste $int")
}
Enter fullscreen mode Exit fullscreen mode

This code is identical to the previous example except for the use of supervisorScope. When you execute it, you will see the same output in the terminal: two "entrou" messages and the exception message. However, now you will also see the result from the teste(int: Int) method. This indicates that when using supervisorScope, even if one coroutine fails, the other continues to run. Changing just one function can significantly alter the behavior of your program.

Conclusion

As I mentioned at the beginning of this post, coroutines are an essential tool for Kotlin developers, but to use them effectively, it’s important to understand their nuances. Both coroutineScope and supervisorScope have their place in production code, and knowing which to use depends on your specific scenario. I hope you found this information helpful! If you'd like to discuss this topic further, feel free to reach out to me on social media.

Thank you!

References

https://kotlinlang.org/docs/coroutines-basics.html#your-first-coroutine
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/

Top comments (1)

Collapse
 
daniel_cavalcanti_b10156f profile image
Daniel Cavalcanti

Thks man !!