Coroutine Context and Scope

Roman Elizarov
Mar 9, 2019 · 4 min read

Different uses of physically near-identical things are usually accompanied by giving those things different names to emphasize the intended purpose. Depending on the use, seamen have a dozen or more words for a rope though it might materially be the same thing. (Wikipedia on Hindley-Milner type system)

Every coroutine in Kotlin has a context that is represented by an instance of CoroutineContext interface. A context is a set of elements and current coroutine context is available via coroutineContext property:

Coroutine context is immutable, but you can add elements to a context using plus operator, just like you add elements to a set, producing a new context instance:

A coroutine itself is represented by a Job. It is responsible for coroutine’s lifecycle, cancellation, and parent-child relations. A current job can be retrieved from a current coroutine’s context:

There is also an interface called CoroutineScope that consists of a sole property — val coroutineContext: CoroutineContext. It has nothing else but a context. So, why it exists and how is it different from a context itself? The difference between a context and a scope is in their intended purpose.

A coroutine is typically launched using launch coroutine builder:

fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
// ...
): Job

It is defined as extension function on CoroutineScope and takes a CoroutineContext as parameter, so it actually takes two coroutine contexts (since a scope is just a reference to a context). What does it do with them? It merges them using plus operator, producing a set-union of their elements, so that the elements in context parameter are taking precedence over the elements from the scope. The resulting context is used to start a new coroutine, but it is not the context of the new coroutine — is the parent context of the new coroutine. The new coroutine creates its own child Job instance (using a job from this context as its parent) and defines its child context as a parent context plus its job:

The intended purpose of CoroutineScope receiver in launch and in all the other coroutine builders is to reference a scope in which new coroutine is launched. By convention, a context in CoroutineScope contains a Job that is going to become a parent of new coroutine (with the exception of GlobalScope that you should avoid anyway¹).

On the other hand, the intended purpose of context: CoroutineContext parameter in launch is to provide additional context elements to override elements that would be otherwise inherited from a parent scope. For example:

By convention, we do not usually pass a Job in a context parameter to launch, since that breaks parent-child relation, unless we explicitly want to break it using a NonCancellable job, for example.

Notice, that a block of code inside launch is defined with CoroutineScope as its receiver:

fun CoroutineScope.launch(
// ...
block: suspend CoroutineScope.() -> Unit
): Job

By convention which is followed by all coroutine builders, this scope’s coroutineContext property is the same as the context of the coroutine that is running inside this block:

This way, when we see an unqualified coroutineContext reference in code, there is no confusion between the correspondingly named top-level property and a scope’s property, since they are the same at all times by design.

IntelliJ IDEA handily marks the block of code inside coroutine builders with this: CoroutineScope hint which lets us immediately distinguish regular code blocks from the blocks with a different context. Moreover, this new CoroutineScope always has a new Job in its context. So, when you see launch { … } in the source code without an explicit receiver you can quickly tell what scope it is launched in by looking for the outer block marked as this: CoroutineScope.

Since the context and the scope are materially the same thing, we could have launched a coroutine without having access to the scope and without using GlobalScope simply by wrapping the current coroutineContext into the instance of CoroutineScope as shown in the following function:

Do not do this! It makes the scope in which the coroutine is launched opaque and implicit, capturing some outer Job to launch a new coroutine without explicitly announcing it in the function signature. A coroutine is a piece of work that is concurrent with the rest of your code and its launch has to be explicit².

If you need to launch a coroutine that keeps running after your function returns, then make your function an extension of CoroutineScope or pass scope: CoroutineScope as parameter to make your intent clear in your function signature. Do not make these functions suspending:

Suspending functions, on the other hand, are designed to be non-blocking and should not have side-effects of launching any concurrent work. Suspending functions can and should wait for all their work to complete before returning to the caller³.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store