“silhouette of trees covered by fogs” by Clément M. on Unsplash

At the heart of Kotlin coroutines is the CoutineContext interface. All the coroutine builder functions like launch and async have the same first parameter, context: CoroutineContext . These coroutine builders are also all defined as extension functions on the CoroutineScope interface, which has a single abstract read-only property, coroutineContext: CoroutineContext .

Every coroutine builder is an extension on CoroutineScope and inherits its coroutineContext to automatically propagate both context elements and cancellation.

CoroutineContext is a fundamental building block of Kotlin coroutines. Being able to manipulate it is therefore vital in order to achieve the correct behavior for threading, life-cycle, exceptions, and debugging.

Structure

It is an indexed set of Element instances. An indexed set is a mix between a set and a map. Every element in this set has a unique Key. Keys are compared by reference.

The API of the CoroutineContext interface might seem obscure at first, but it’s actually just a type-safe heterogeneous map, from CoroutineContext.Key instances (compared by reference, not value, as per the class documentation) to CoroutineContext.Element instances. To understand why a new interface had to be redefined rather than simply using a standard Map , consider the equivalent declaration of the context.

typealias CoroutineContext =

Map<CoroutineContext.Key<*>, CoroutineContext.Element>

The get method is then incapable of inferring the Element in the response from the key which was used, even though this information is actually available in the generic type of the key.

fun get(key: CoroutineContext.Key<*>): CoroutineContext.Element?

So, whenever an element is fetched from the map, it needs to be cast to the actual type. But in the CoroutineContext class, the generic get method actually defines the returned Element type based on the generic type of the Key passed as argument.

fun <E : Element> get(key: Key<E>): E?

This way elements can safely be fetched without the need to type-cast, because their type was specified in the key which was used.

Operations

CoroutineContext doesn’t implement a collection interface, so it doesn’t have the typical collection operators. There is one important operator though, plus . The plus operator combines CoroutineContext instances with each other. This will merge the elements they contain, overwriting the elements in the context on the left-hand side of the operator with the ones in the context on the right-hand side, much like the behavior on Map .

[The plus operator] returns a context containing elements from this context and elements from other context. The elements from this context with the same key as in the other one are dropped.

The CoroutineContext.Element interface actually inherits CoroutineContext . This is handy because it means that CoroutineContext.Element instances can be simply treated as CoroutineContext s containing a single element, themselves.

An element of the coroutine context is a singleton context by itself.

With this, the + operator can be used to easily combine contexts with elements and elements with each other into a new context. The important thing to watch out for is the order in which they are combined, since the + operator is asymmetrical.

In cases where a context shouldn’t hold any elements, the EmptyCoroutineContext object can be used. As can be expected, adding this object to any other context has no effect on that context.

Elements

As explained, CoroutineContext is essentially a map, and it always holds a predefined set of items. Since all the keys have to implement the CoroutineContext.Key interface, it’s easy to find the list of public Element s by searching through the source code for the implementations of CoroutineContext.Key and checking which Element class they’re associated with.

ContinuationInterceptor is invoked for continuations, to manage the underlying execution threads. In practice, implementations always extend the CoroutineDispatcher base class.

is invoked for continuations, to manage the underlying execution threads. In practice, implementations always extend the CoroutineDispatcher base class. Job models the life-cycle and task hierarchy in which a coroutine is being executed.

models the life-cycle and task hierarchy in which a coroutine is being executed. CoroutineExceptionHandler is used by coroutine builders which don’t propagate exceptions, namely launch and actor , in order to determine what to do if an exception is encountered. See CoroutineExceptionHandler in the guide for more details.

is used by coroutine builders which don’t propagate exceptions, namely and , in order to determine what to do if an exception is encountered. See CoroutineExceptionHandler in the guide for more details. CoroutineName is used for debugging purposes. See Naming coroutines for debugging in the guide for more details.

Each key is defined as the companion object of its associated Element interface or class. This way, keys can be directly referred to by using the element type names. For example, coroutineContext[Job] will return the instance of Job held by coroutineContext , or null if it doesn’t contain any.

If extensibility wasn’t a factor, CoroutineContext could simply be modelled as a class.

class CoroutineContext(

val continuationInterceptor: ContinuationInterceptor?,

val job: Job?,

val coroutineExceptionHandler: CoroutineExceptionHandler,

val name: CoroutineName?

)

CoroutineScope builders

When we want to start a coroutine, we need to call a builder function on a CoroutineScope instance. In the builder function, we can actually see three contexts coming into play.

The CoroutineScope receiver is defined by the way it provides a CoroutineContext . This is the inherited context .

receiver is defined by the way it provides a . This is the . The builder function receives a CoroutineContext instance in its first parameter. We’ll call this the context argument .

instance in its first parameter. We’ll call this the . The suspending block parameter in the builder function has a CoroutineScope receiver, which itself also provides a CoroutineContext . This is the coroutine context.

Looking at the source for launch and async , they both start we the same statement.

val newContext = newCoroutineContext(context)

The newCoroutineContext extension on CoroutineScope handles merging the inherited context with the context argument, as well as providing default values and doing a bit of extra configuration. The merge is written as coroutineContext + context , where coroutineContext is the inherited context and context is the context argument. Given what’s been explained previously about the CoroutineContext.plus operator, the right-hand operand takes precedence, therefore the attributes from the context argument will overwrite those in the inherited context. The result is the parent context.

parent context = default values + inherited context + context argument

The CoroutineScope instance passed as receiver to the suspending block is actually the coroutine itself, always inheriting AbstractCoroutine , which implements CoroutineScope and is also a Job . The coroutine context is provided by this class and will return the parent context obtained previously, to which it adds itself, effectively overriding the Job .

coroutine context = parent context + coroutine job

Defaults

When elements are missing from a context which is being used by a coroutine, it uses a default value.

The default ContinuationInterceptor is Dispatchers.Default . This is documented in newCoroutineContext . Therefore if neither the inherited context nor the context parameter have a dispatcher, then the default dispatcher is used. In that case, the coroutine context will also inherit the default dispatcher.

is . This is documented in . Therefore if neither the inherited context nor the context parameter have a dispatcher, then the default dispatcher is used. In that case, the coroutine context will also inherit the default dispatcher. If the context doesn’t have a Job , then the coroutine which is created doesn’t have a parent.

, then the coroutine which is created doesn’t have a parent. If the context doesn’t have a CoroutineExceptionHandler , then the global handler is used (but not installed in the context). This will ultimately call handleCoroutineExceptionImpl which first uses the java ServiceLoader to load all implementations of CoroutineExceptionHandler , then propagate the exception to the current thread’s uncaught exception handler. On Android, a special exception handler is automatically installed to report exceptions to the hidden uncaughtExceptionPreHandler property on Thread , which logs exceptions to the console.

, then the global handler is used (but not installed in the context). This will ultimately call which first uses the java ServiceLoader to load all implementations of , then propagate the exception to the current thread’s uncaught exception handler. On Android, a special exception handler is automatically installed to report exceptions to the hidden property on , which logs exceptions to the console. The default name for a coroutine is "coroutine" , hardcoded at places where the CoroutineName key is used to fetch the name from a context.

Looking at the modelling of CoroutineScope as a class, it can be clarified with default values.

val defaultExceptionHandler = CoroutineExceptionHandler { ctx, t ->

ServiceLoader.load(

serviceClass,

serviceClass.classLoader

).forEach{

it.handleException(ctx, t)

}

Thread.currentThread().let {

it.uncaughtExceptionHandler.uncaughtException(it, exception)

}

} class CoroutineContext(

val continuationInterceptor: ContinuationInterceptor =

Dispatchers.Default,

val parentJob: Job? =

null,

val coroutineExceptionHandler: CoroutineExceptionHandler =

defaultExceptionHandler,

val name: CoroutineName =

CoroutineName("coroutine")

)

Usage

Through some examples, let’s look at the resulting context in some coroutine expressions, and most importantly which dispatchers and parent jobs they inherit.

Global Scope Context

GlobalScope.launch {

/* ... */

}

If we check the source of GlobalScope , we see that its implementation of coroutineContext always returns an EmptyCoroutineContext . The resulting context used by the coroutine will therefore use all the default values. The above statement is for example identical to the following one, where the default dispatcher is explicitly specified.

GlobalScope.launch(Dispatchers.Default) {

/* ... */

}

Fully Qualified Context

Inversely, we can specify all the elements within a context passed as argument.

launch(

Dispatchers.Main +

Job() +

CoroutineName("HelloCoroutine") +

CoroutineExceptionHandler { _, _ -> /* ... */ }

) {

/* ... */

}

None of the elements from the inherited context will be actually be taken into account. This statement has the same behavior no matter the CoroutineScope on which it’s called.

CoroutineScope Context

In the coroutines UI programming guide for Android, we find the following example in the Structured concurrency, lifecycle and coroutine parent-child hierarchy section, showing how to implement a CoroutineScope in an Activity .

abstract class ScopedAppActivity:

AppCompatActivity(),

CoroutineScope

{

protected lateinit var job: Job

override val coroutineContext: CoroutineContext

get() = job + Dispatchers.Main



override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

job = Job()

}



override fun onDestroy() {

super.onDestroy()

job.cancel()

}

}

In this suggestion, the context returned by this scope has a dispatcher. This is a design choice, so that all coroutine builders called on this scope will use the Main dispatcher rather than Default . Defining elements in the scope’s context is a way to override the library defaults in the places where the context is used. The scope also provides a job , so that all coroutines launched from this context have the same parent. This way, there’s a single point from which to cancel them all.

Overriding Parent Job

We can have some context elements inherited from scope and other added in the context parameter, so that the two are combined. For example, when using the NonCancellable job, it’s typically the only element in the context passed as argument.

withContext(NonCancellable) {

/* ... */

}

The code executed within this block will inherit the dispatcher from its calling context, but it will override that context’s job by using NonCancellable as parent. This way, the coroutine will always be in an active state.

Accessing Context Elements

The elements in the current context can be obtained by using the top-level suspending coroutineContext read-only property.

println("Running in ${coroutineContext[CoroutineName]}")

The above statement can for example be used to print the name of the current coroutine.

If we want we can actually rebuild a coroutine context identical to the current one, from its individual elements.

val inheritedContext = sequenceOf(

Job,

ContinuationInterceptor,

CoroutineExceptionHandler,

CoroutineName

)

.mapNotNull { key -> coroutineContext[key] }

.fold(EmptyCoroutineContext) { ctx: CoroutineContext, elt ->

ctx + elt

}

launch(inheritedContext) {

/* ... */

}

Although interesting to understand the composition of the context, this example is completely useless in practice. We would obtain exactly the same behavior by leaving the context parameter of launch to its default empty value.

Nested Context

This last example is important because it presents a change of behavior in the latest releases of coroutines, where the builder functions became extensions on CoroutineScope .

GlobalScope.launch(Dispatchers.Main) {

val deferred = async {

/* ... */

}

/* ... */

}

Given that async is called on the scope (instead of being a top-level function), it will inherit the scope’s dispatcher, specified as Dispatchers.Main by launch , rather than using the default one. In previous versions of coroutines, the code within async would run on a worker thread provided by Dispatchers.Default , but it will now run on the UI thread, which could cause the application to hang or even crash.

The solution is simply to be more explicit about the dispatcher being used in async .

launch(Dispatchers.Main) {

val deferred = async(Dispatchers.Default) {

/* ... */

}

/* ... */

}

Coroutine API Design