Scoping functions in Kotlin Coroutines

This is a chapter from the book Kotlin Coroutines. You can find Early Access on LeanPub.

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

Approaches before scoping functions

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, the function will take 2 seconds instead of 1).

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

To make two suspending calls concurrently, the easiest way is by wrapping them with async. Although 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(), 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. It means:

  • it cannot be canceled (if the parent would be canceled, functions inside async would still be running, wasting resources until they are done),
  • it is not inheriting 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 unnecessary calculations,
  • the tools for unit testing coroutines will not work here, and so testing this function is very hard.

This is not a good solution. Let's take a look at another one, in which we are passing 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(), notifications = notifications.await(), ) } // or // DON'T DO THAT fun CoroutineScope.getUserProfile(): UserProfileData { val user = async { getUserData() } val notifications = async { getNotifications() } return UserProfileData( user = user.await(), notifications = notifications.await(), ) }

This one is a bit better, as cancellation and proper unit testing are now possible. The problem is that it requires passing this scope from function to function. Also, such functions can cause unwanted side effects on the scope - for instance, if there would be an exception in one async, the whole scope would 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 see tweets, if we have a problem calculating user details. Apparently, an exception in async broke the whole scope and ended the program. Instead, we would prefer a function that in case of an exception just throws it. Time to introduce our hero: coroutineScope.

coroutineScope

coroutineScope is a provided suspending function that starts a scope. It returns a value produced by the argument function (most often lambda expression).

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

Unlike async or launch, scoping functions do not really create new coroutines. Their code block is called in-place. When they are suspended, we suspend a coroutine on which this scoping function is called. Take a look at the below example - both delay calls suspend runBlocking.

import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import 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 overrides the context's Job. This way, the produced scope respects parental responsibilities:

  • inherits a context from its parent,
  • awaits for all children before it can finish itself,
  • cancels all its children, when the parent is canceled.

In the below example you can observe that "After" will be printed on 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. Canceled parent leads to unfinished child cancellation.

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 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(), notifications = notifications.await(), ) }

As we've already mentioned, coroutineScope is nowadays often used to wrap 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. It is a useful function, but not alone of its type. Other similar functions we need to know are withContext and supervisorScope. Those, and other functions that are making a scope, but are not coroutine builders, are called scoping functions.

withContext

The function withContext is similar to coroutineScope, which additionally allows making some changes on the scope. The context provided as an argument to this function is added to the context from the parent scope (the same way as in coroutine builders). This means that withContext(EmptyCoroutineContext) and coroutineScope() behave 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

Function withContext is often used to set a different coroutine scope for part of our code. Most often together with dispatchers, that 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 how coroutineScope { /*...*/ } works is very similar to async with immediate async { /*...*/ }.await(). Also withContext(context) { /*...*/ } is in a way similar to async(context) { /*...*/ }.await(). The biggest difference is that async requires scope, where coroutineScope and withContext take it from suspension. In both cases prefer coroutineScope and withContext, and avoid async with immediate await.

supervisorScope

The function supervisorScope also behaves a lot like coroutineScope - creates a CoroutineScope that inherits from the outer scope, and calls the specified suspend block with this scope. The difference is that it overrides context's Job with SupervisorJob, so it is not canceled 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 // Exception... // 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) } } }

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 long time behaves just like coroutineScope. The difference is that withTimeout additionally sets time limit for its body execution. If it takes too long, it cancels this body, and throws TimeoutCancellationException (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 } // Still thinking // Cancelled

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

// will not start, because runBlockingTest requires kotlinx-coroutines-test, but you can copy it to your project import kotlinx.coroutines.* import kotlinx.coroutines.test.runBlockingTest import org.junit.Test class Test { @Test fun testTime2() = runBlockingTest { withTimeout(1000) { // something that should less than 1000 delay(900) // virtual time } } @Test(expected = TimeoutCancellationException::class) fun testTime1() = runBlockingTest { 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 since withTimeout throws TimeoutCancellationException, that is a subtype of CancellationException. So when this exception is thrown in a coroutine builder, it only cancels it, without affecting its parent.

import kotlinx.coroutines.* suspend fun main(): Unit = coroutineScope { launch { launch { // cancelled by its parent delay(2000) println("Will not be printed") } withTimeout(1000) { // we cancel launch delay(1500) } } launch { delay(2000) println("Done") } } // (2 sec) // Done
  1. delay(1500) takes longer that withTimeout(1000) expects, so it throws TimeoutCancellationException.
  2. The exception is caught by launch from line 2, and it cancels itself and its children, so also launch from line 3.
  3. launch from line 11 is not affected.

A less aggressive variant of withTimeout is withTimeoutOrNull. It does not throw an exception. In case of exceeded timeout, it just cancels its body and returns null. I find withTimeoutOrNull useful to wrap functions in which too long waiting time signalize that something went wrong. For instance network operations - if we wait over 5 seconds for response, it is unlikely we will ever receive it.

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

Summary

Scoping 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 see their usefulness. They are a very important part of Kotlin Coroutines ecosystem. You will see how we will use them through the rest of the book.