article banner

Launching coroutines vs suspend functions

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

Let's consider a use case where you need to perform a number of concurrent actions. There are two kinds of functions that can be used for this:

  • a regular function operating on a coroutine scope object,
  • a suspending function.
class NotificationsSender( private val client: NotificationsClient, private val notificationScope: CoroutineScope, ) { fun sendNotifications( notifications: List<Notification> ) { for (n in notifications) { notificationScope.launch { client.send(n) } } } }
class NotificationsSender( private val client: NotificationsClient, ) { suspend fun sendNotifications( notifications: List<Notification> ) = supervisorScope { for (n in notifications) { launch { client.send(n) } } } }

These two options might look similar in some ways, but they represent essentially different use cases. To see some essential differences, you don’t even need to read their bodies.

When we know that a regular function starts coroutines, we know it needs to use a scope object. Such functions typically only start coroutines and don’t await their completion, sosendNotifications will most likely take milliseconds to execute. Starting coroutines on an external scope also means that exceptions in these coroutines will be handled by this scope (most often printed and ignored). These coroutines will inherit context from the scope object, so we need to cancel the scope to cancel them.

class NotificationsSender( private val client: NotificationsClient, private val notificationScope: CoroutineScope, ) { // Does not wait for started coroutines // Exceptions are handled by the scope // Takes context from the scope // and builds relationship to the scope fun sendNotifications( notifications: List<Notification> ) { // ... } }

When we know that a suspending function starts coroutines, we can assume that this function will not finish until all the coroutines are finished. Such sendNotifications will suspend until the last notification is fully handled. This is an important synchronization mechanism. We also know that suspending functions don’t cancel their parent. Instead, they might throw or silence exceptions, just like regular functions can throw or silence exceptions. The coroutines started by such a function should inherit context from and build relationships with the function that called them.

class NotificationsSender( private val client: NotificationsClient, ) { // Waits for its coroutines // Handles exceptions // Takes context and builds relationship to // the coroutine that started it suspend fun sendNotifications( notifications: List<Notification> ) { // ... } }

Both these use cases are important in our applications. When we have a choice, we should prefer suspending functions as they are easier to control and synchronize. But coroutines need to start somewhere, and for that we use regular functions with a scope object.

In some applications, we might have a situation where we need to mix these two kinds of functions when we need suspending functions that also start external processes on an outer scope.

Consider a function that needs to execute some operations that are an essential part of its execution, but it also needs to start an independent process. In the example below, the process of sending an event is considered an external process, so it is started on an external scope. Thanks to this:

  • updateUser will not wait until the sendEvent execution is completed.
  • the sendEvent process will not be cancelled when the coroutine that started updateUser is cancelled. (This makes sense because user synchronization has completed anyway.)
  • eventsScope decides what the context should be for sending events, and whether or not this event should happen. (If eventsScope is not active, no event will be sent.)
suspend fun updateUser() = coroutineScope { val apiUserAsync = async { api.fetchUser() } val dbUserAsync = async { db.getUser() } val apiUser = apiUserAsync.await() val dbUser = dbUserAsync.await() if (apiUser.lastUpdate > dbUser.lastUpdate) { db.updateUser(apiUser) } { api.updateUser(dbUser) } eventsScope.launch { sendEvent(UserSunchronized) } }

In some situations, this hybrid behavior is what we want in our applications, but it should be used wisely.