article banner (priority)

Coroutine scope functions

This is a chapter from the book Kotlin Coroutines. You can find it on LeanPub or Amazon.

Imagine that in a suspending function you need to concurrently get data from two (or more) endpoints. Before we explore how to do this correctly, let's see some suboptimal approaches.

Approaches that were used before coroutine scope functions were introduced

The first approach is calling suspending functions from a suspending function. The problem with this solution is that it is not concurrent (so, if getting data from one endpoint takes 1 second, a function will take 2 seconds instead of 1).

// Data loaded sequentially, not simultaneously suspend fun getUserProfile(): UserProfileData { val user = getUserData() // (1 sec) val notifications = getNotifications() // (1 sec) return UserProfileData( user = user, notifications = notifications, ) }

To make two suspending calls concurrently, the easiest way is by wrapping them with async. However, async requires a scope, and using GlobalScope is not a good idea.

// DON'T DO THAT suspend fun getUserProfile(): UserProfileData { val user = GlobalScope.async { getUserData() } val notifications = GlobalScope.async { getNotifications() } return UserProfileData( user = user.await(), // (1 sec) notifications = notifications.await(), ) }

GlobalScope is just a scope with EmptyCoroutineContext.

public object GlobalScope : CoroutineScope { override val coroutineContext: CoroutineContext get() = EmptyCoroutineContext }

If we call async on a GlobalScope, we will have no relationship to the parent coroutine. This means that the async coroutine:

  • cannot be cancelled (if the parent is cancelled, functions inside async still run, thus wasting resources until they are done);
  • does not inherit a scope from any parent (it will always run on the default dispatcher and will not respect any context from the parent).

The most important consequences are:

  • potential memory leaks and redundant CPU usage;
  • the tools for unit testing coroutines will not work here, so testing this function is very hard.

This is not a good solution. Let's take a look at another one, in which we pass a scope as an argument:

// DON'T DO THAT suspend fun getUserProfile( scope: CoroutineScope ): UserProfileData { val user = scope.async { getUserData() } val notifications = scope.async { getNotifications() } return UserProfileData( user = user.await(), // (1 sec) notifications = notifications.await(), ) } // or // DON'T DO THAT suspend fun CoroutineScope.getUserProfile(): UserProfileData { val user = async { getUserData() } val notifications = async { getNotifications() } return UserProfileData( user = user.await(), // (1 sec) notifications = notifications.await(), ) }

This one is a bit better as cancellation and proper unit testing are now possible. The problem is that this solution requires this scope to be passed from function to function. Also, such functions can cause unwanted side effects in the scope; for instance, if there is an exception in one async, the whole scope will be shut down (assuming it is using Job, not SupervisorJob). What is more, a function that has access to the scope could easily abuse this access and, for instance, cancel this scope with the cancel method. This is why this approach can be tricky and potentially dangerous.

import kotlinx.coroutines.* //sampleStart data class Details(val name: String, val followers: Int) data class Tweet(val text: String) fun getFollowersNumber(): Int = throw Error("Service exception") suspend fun getUserName(): String { delay(500) return "marcinmoskala" } suspend fun getTweets(): List<Tweet> { return listOf(Tweet("Hello, world")) } suspend fun CoroutineScope.getUserDetails(): Details { val userName = async { getUserName() } val followersNumber = async { getFollowersNumber() } return Details(userName.await(), followersNumber.await()) } fun main() = runBlocking { val details = try { getUserDetails() } catch (e: Error) { null } val tweets = async { getTweets() } println("User: $details") println("Tweets: ${tweets.await()}") } // Only Exception... //sampleEnd

In the above code, we would like to at least see Tweets, even if we have a problem fetching user details. Unfortunately, an exception on getFollowersNumber breaks async, which breaks the whole scope and ends the program. Instead, we would prefer a function that just throws an exception if it occurs. Time to introduce our hero: coroutineScope.

coroutineScope

coroutineScope is a suspending function that starts a scope. It returns the value produced by the argument function.

suspend fun <R> coroutineScope( block: suspend CoroutineScope.() -> R ): R

Unlike async or launch, the body of coroutineScope is called in-place. It formally creates a new coroutine, but it suspends the previous one until the new one is finished, so it does not start any concurrent process. Take a look at the below example, in which both delay calls suspend runBlocking.

import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking //sampleStart fun main() = runBlocking { val a = coroutineScope { delay(1000) 10 } println("a is calculated") val b = coroutineScope { delay(1000) 20 } println(a) // 10 println(b) // 20 } // (1 sec) // a is calculated // (1 sec) // 10 // 20 //sampleEnd

The provided scope inherits its coroutineContext from the outer scope, but it overrides the context's Job. Thus, the produced scope respects its parental responsibilities:

  • inherits a context from its parent;
  • waits for all its children before it can finish itself;
  • cancels all its children when the parent is cancelled.

In the example below, you can observe that "After" will be printed at the end because coroutineScope will not finish until all its children are finished. Also, CoroutineName is properly passed from parent to child.

import kotlinx.coroutines.* //sampleStart suspend fun longTask() = coroutineScope { launch { delay(1000) val name = coroutineContext[CoroutineName]?.name println("[$name] Finished task 1") } launch { delay(2000) val name = coroutineContext[CoroutineName]?.name println("[$name] Finished task 2") } } fun main() = runBlocking(CoroutineName("Parent")) { println("Before") longTask() println("After") } // Before // (1 sec) // [Parent] Finished task 1 // (1 sec) // [Parent] Finished task 2 // After //sampleEnd

In the next snippet, you can observe how cancellation works. A cancelled parent leads to the cancellation of unfinished children.

import kotlinx.coroutines.* //sampleStart suspend fun longTask() = coroutineScope { launch { delay(1000) val name = coroutineContext[CoroutineName]?.name println("[$name] Finished task 1") } launch { delay(2000) val name = coroutineContext[CoroutineName]?.name println("[$name] Finished task 2") } } fun main(): Unit = runBlocking { val job = launch(CoroutineName("Parent")) { longTask() } delay(1500) job.cancel() } // [Parent] Finished task 1 //sampleEnd

Unlike coroutine builders, if there is an exception in coroutineScope or any of its children, it cancels all other children and rethrows it. This is why using coroutineScope would fix our previous "Twitter example". To show that the same exception is rethrown, I changed a generic Error into a concrete ApiException.

import kotlinx.coroutines.* //sampleStart data class Details(val name: String, val followers: Int) data class Tweet(val text: String) class ApiException( val code: Int, message: String ) : Throwable(message) fun getFollowersNumber(): Int = throw ApiException(500, "Service unavailable") suspend fun getUserName(): String { delay(500) return "marcinmoskala" } suspend fun getTweets(): List<Tweet> { return listOf(Tweet("Hello, world")) } suspend fun getUserDetails(): Details = coroutineScope { val userName = async { getUserName() } val followersNumber = async { getFollowersNumber() } Details(userName.await(), followersNumber.await()) } fun main() = runBlocking<Unit> { val details = try { getUserDetails() } catch (e: ApiException) { null } val tweets = async { getTweets() } println("User: $details") println("Tweets: ${tweets.await()}") } // User: null // Tweets: [Tweet(text=Hello, world)] //sampleEnd

This all makes coroutineScope a perfect candidate for most cases when we just need to start a few concurrent calls in a suspending function.

suspend fun getUserProfile(): UserProfileData = coroutineScope { val user = async { getUserData() } val notifications = async { getNotifications() } UserProfileData( user = user.await(), // (1 sec) notifications = notifications.await(), ) }

As we've already mentioned, coroutineScope is nowadays often used to wrap a suspending main body. You can think of it as the modern replacement for the runBlocking function:

import kotlinx.coroutines.* //sampleStart suspend fun main(): Unit = coroutineScope { launch { delay(1000) println("World") } println("Hello, ") } // Hello // (1 sec) // World //sampleEnd

The function coroutineScope creates a scope out of a suspending context. It inherits a scope from its parent and supports structured concurrency.

To make it clear, there is practically no difference between the below functions, except that the first one calls getProfile and getFriends sequentially, where the second one calls them simultaneously.

suspend fun produceCurrentUserSeq(): User { val profile = repo.getProfile() val friends = repo.getFriends() return User(profile, friends) } suspend fun produceCurrentUserSym(): User = coroutineScope { val profile = async { repo.getProfile() } val friends = async { repo.getFriends() } User(profile.await(), friends.await()) }

coroutineScope is a useful function, but it’s not the only one of its kind.

Coroutine scope functions

There are more functions that create a scope and behave similarly to coroutineScope. supervisorScope is like coroutineScope but it uses SupervisorJob instead of Job. withContext is a coroutineScope that can modify coroutine context. withTimeout is a coroutineScope with a timeout. Each of those functions will be better explained in the following parts of this chapter. For now, I just want you to know there are such functions because if there is a group of similar functions, it makes sense that it should have a name. So how should we name this group? Some people call them "scoping functions", but I find this confusing as I am not sure what is meant by "scoping". I guess that whoever started using this term just wanted to make it different from "scope functions" (functions like let, with or apply). It is not really helpful as those two terms are still often confused. This is why I decided to use the term "coroutine scope functions". It is longer but should cause fewer misunderstandings, and I find it more correct. Just think about that: coroutine scope functions are those that are used to create a coroutine scope in suspending functions.

On the other hand, coroutine scope functions are often confused with coroutine builders, but this is incorrect because they are very different, both conceptually and practically. To clarify this, the table below presents the comparison between them.

Coroutine builders (except for runBlocking)Coroutine scope functions
launch, async, producecoroutineScope, supervisorScope, withContext, withTimeout
Are extension functions on CoroutineScope.Are suspending functions.
Take coroutine context from CoroutineScope receiver.Take coroutine context from suspending function continuation.
Exceptions are propagated to the parent through Job.Exceptions are thrown in the same way as they are from/by regular functions.
Starts an asynchronous coroutine.Starts a coroutine that is called in-place.

Now think about runBlocking. You might notice that it looks like it has more in common with coroutine scope functions than with builders. runBlocking also calls its body in-place and returns its result. The biggest difference is that runBlocking is a blocking function, while coroutine scope functions are suspending functions. This is why runBlocking must be at the top of the hierarchy of coroutines, while coroutine scope functions must be in the middle.

withContext

The withContext function is similar to coroutineScope, but it additionally allows some changes to be made to the scope. The context provided as an argument to this function overrides the context from the parent scope (the same way as in coroutine builders). This means that withContext(EmptyCoroutineContext) and coroutineScope() behave in exactly the same way.

import kotlinx.coroutines.* //sampleStart fun CoroutineScope.log(text: String) { val name = this.coroutineContext[CoroutineName]?.name println("[$name] $text") } fun main() = runBlocking(CoroutineName("Parent")) { log("Before") withContext(CoroutineName("Child 1")) { delay(1000) log("Hello 1") } withContext(CoroutineName("Child 2")) { delay(1000) log("Hello 2") } log("After") } // [Parent] Before // (1 sec) // [Child 1] Hello 1 // (1 sec) // [Child 2] Hello 2 // [Parent] After //sampleEnd

The function withContext is often used to set a different coroutine scope for part of our code. Usually, you should use it together with dispatchers, as will be described in the next chapter.

launch(Dispatchers.Main) { view.showProgressBar() withContext(Dispatchers.IO) { fileRepository.saveData(data) } view.hideProgressBar() }

You might notice that the way coroutineScope { /*...*/ } works very similar to async with immediate await: async { /*...*/ }.await(). Also withContext(context) { /*...*/ } is in a way similar to async(context) { /*...*/ }.await(). The biggest difference is that async requires a scope, where coroutineScope and withContext take the scope from suspension. In both cases, it’s better to use coroutineScope and withContext, and avoid async with immediate await.

supervisorScope

The supervisorScope function also behaves a lot like coroutineScope: it creates a CoroutineScope that inherits from the outer scope and calls the specified suspend block in it. The difference is that it overrides the context's Job with SupervisorJob, so it is not cancelled when a child raises an exception.

import kotlinx.coroutines.* //sampleStart fun main() = runBlocking { println("Before") supervisorScope { launch { delay(1000) throw Error() } launch { delay(2000) println("Done") } } println("After") } // Before // (1 sec) // Exception... // (1 sec) // Done // After //sampleEnd

supervisorScope is mainly used in functions that start multiple independent tasks.

suspend fun notifyAnalytics(actions: List<UserAction>) = supervisorScope { actions.forEach { action -> launch { notifyAnalytics(action) } } }

If you use async, silencing its exception propagation to the parent is not enough. When we call await and the async coroutine finishes with an exception, then await will rethrow it. This is why if we want to truly ignore exceptions, we should also wrap await calls with a try-catch block.

class ArticlesRepositoryComposite( private val articleRepositories: List<ArticleRepository>, ) : ArticleRepository { override suspend fun fetchArticles(): List<Article> = supervisorScope { articleRepositories .map { async { it.fetchArticles() } } .mapNotNull { try { it.await() } catch (e: Throwable) { e.printStackTrace() null } } .flatten() .sortedByDescending { it.publishedAt } } }

In my workshops, I am often asked if we can use withContext(SupervisorJob()) instead of supervisorScope. No, we can't. When we use withContext(SupervisorJob()), then withContext is still using a regular Job, and the SupervisorJob() becomes its parent. As a result, when one child raises an exception, the other children will be cancelled as well. withContext will also throw an exception, so its SupervisorJob() is practically useless. This is why I find withContext(SupervisorJob()) pointless and misleading, and I consider it a bad practice.

import kotlinx.coroutines.* //sampleStart fun main() = runBlocking { println("Before") withContext(SupervisorJob()) { launch { delay(1000) throw Error() } launch { delay(2000) println("Done") } } println("After") } // Before // (1 sec) // Exception... //sampleEnd

withTimeout

Another function that behaves a lot like coroutineScope is withTimeout. It also creates a scope and returns a value. Actually, withTimeout with a very big timeout behaves just like coroutineScope. The difference is that withTimeout additionally sets a time limit for its body execution. If it takes too long, it cancels this body and throws TimeoutCancellationException (a subtype of CancellationException).

import kotlinx.coroutines.* suspend fun test(): Int = withTimeout(1500) { delay(1000) println("Still thinking") delay(1000) println("Done!") 42 } suspend fun main(): Unit = coroutineScope { try { test() } catch (e: TimeoutCancellationException) { println("Cancelled") } delay(1000) // Extra timeout does not help, // `test` body was cancelled } // (1 sec) // Still thinking // (0.5 sec) // Cancelled

The function withTimeout is especially useful for testing. It can be used to test if some function takes more or less than some time. If it is used inside runTest, it will operate in virtual time. We also use it inside runBlocking to just limit the execution time of some function (this is then like setting timeout on @Test).

// will not start, because runTest requires kotlinx-coroutines-test, but you can copy it to your project import kotlinx.coroutines.* import kotlinx.coroutines.test.runTest import org.junit.Test class Test { @Test fun testTime2() = runTest { withTimeout(1000) { // something that should take less than 1000 delay(900) // virtual time } } @Test(expected = TimeoutCancellationException::class) fun testTime1() = runTest { withTimeout(1000) { // something that should take more than 1000 delay(1100) // virtual time } } @Test fun testTime3() = runBlocking { withTimeout(1000) { // normal test, that should not take too long delay(900) // really waiting 900 ms } } }

Beware that withTimeout throws TimeoutCancellationException, which is a subtype of CancellationException (the same exception that is thrown when a coroutine is cancelled). So, when this exception is thrown in a coroutine builder, it only cancels it and does not affect its parent (as explained in the previous chapter).

import kotlinx.coroutines.* suspend fun main(): Unit = coroutineScope { launch { // 1 launch { // 2, cancelled by its parent delay(2000) println("Will not be printed") } withTimeout(1000) { // we cancel launch delay(1500) } } launch { // 3 delay(2000) println("Done") } } // (2 sec) // Done

In the above example, delay(1500) takes longer than withTimeout(1000) expects, so it throws TimeoutCancellationException. The exception is caught by launch from 1, and it cancels itself and its children, so launch from 2. launch started at 3 is also not affected.

A less aggressive variant of withTimeout is withTimeoutOrNull, which does not throw an exception. If the timeout is exceeded, it just cancels its body and returns null. I find withTimeoutOrNull useful for wrapping functions in which waiting times that are too long signal that something went wrong. For instance, network operations: if we wait over 5 seconds for a response, it is unlikely we will ever receive it (some libraries might wait forever).

import kotlinx.coroutines.* class User() suspend fun fetchUser(): User { // Runs forever while (true) { yield() } } suspend fun getUserOrNull(): User? = withTimeoutOrNull(5000) { fetchUser() } suspend fun main(): Unit = coroutineScope { val user = getUserOrNull() println("User: $user") } // (5 sec) // User: null

Connecting coroutine scope functions

If you need to use functionalities from two coroutine scope functions, you need to use one inside another. For instance, to set both a timeout and a dispatcher, you can use withTimeoutOrNull inside withContext.

suspend fun calculateAnswerOrNull(): User? = withContext(Dispatchers.Default) { withTimeoutOrNull(1000) { calculateAnswer() } }

Additional operations

Imagine a case in which in the middle of some processing you need to execute an additional operation. For example, after showing a user profile you want to send a request for analytics purposes. People often do this with just a regular launch in the same scope:

class ShowUserDataUseCase( private val repo: UserDataRepository, private val view: UserDataView, ) { suspend fun showUserData() = coroutineScope { val name = async { repo.getName() } val friends = async { repo.getFriends() } val profile = async { repo.getProfile() } val user = User( name = name.await(), friends = friends.await(), profile = profile.await() ) view.show(user) launch { repo.notifyProfileShown() } } }

However, there are some problems with this approach. Firstly, this launch does nothing here because coroutineScope needs to await its completion anyway. So if you are showing a progress bar when updating the view, the user needs to wait until this notifyProfileShown is finished as well. This does not make much sense.

fun onCreate() { viewModelScope.launch { _progressBar.value = true showUserData() _progressBar.value = false } }

The second problem is cancellation. Coroutines are designed (by default) to cancel other operations when there is an exception. This is great for essential operations. If getProfile has an exception, we should cancel getName and getFriends because their response would be useless anyway. However, canceling a process just because an analytics call has failed does not make much sense.

So what should we do? When you have an additional (non-essential) operation that should not influence the main process, it is better to start it on a separate scope. Creating your own scope is easy. In this example, we create an analyticsScope.

val analyticsScope = CoroutineScope(SupervisorJob())

For unit testing and controlling this scope, it is better to inject it via a constructor:

class ShowUserDataUseCase( private val repo: UserDataRepository, private val view: UserDataView, private val analyticsScope: CoroutineScope, ) { suspend fun showUserData() = coroutineScope { val name = async { repo.getName() } val friends = async { repo.getFriends() } val profile = async { repo.getProfile() } val user = User( name = name.await(), friends = friends.await(), profile = profile.await() ) view.show(user) analyticsScope.launch { repo.notifyProfileShown() } } }

Starting operations on an injected scope object is common. Passing a scope clearly signals that such a class can start independent calls. This means suspending functions might not wait for all the operations they start. If no scope is passed, we can expect that suspending functions will not finish until all their operations are done.

Summary

Coroutine scope functions are really useful, especially since they can be used in any suspending function. Most often they are used to wrap the whole function body. Although they are often used to just wrap a bunch of calls with a scope (especially withContext), I hope you can appreciate their usefulness. They are a very important part of the Kotlin Coroutines ecosystem. You will see how we will use them through the rest of the book.