skip to content

How does the + operator combine coroutine contexts, and what are its precedence/replacement rules when the same kind of element appears twice?

level: middleimportance: should knowfreq 40%

answer

  1. builds a new immutable CombinedContext
  2. Same key: right operand wins (override)
  3. Distinct keys coexist
  4. EmptyCoroutineContext is the identity
  5. Builder context = parent + passed + fresh child Job

basics

~20 s

The + operator merges two contexts into a new one. If both sides contain the same kind of element (say two dispatchers), the one on the right wins and replaces the one on the left.

solid answer

~40 s

CoroutineContext.plus produces a new immutable CombinedContext from left + right. Elements are matched by Key; for any key present on both sides, the right operand's element overrides the left's. So Dispatchers.Default + Dispatchers.IO yields a context whose dispatcher is IO. EmptyCoroutineContext is the identity: ctx + EmptyCoroutineContext == ctx. plus also has a special rule for CoroutineExceptionHandler/interceptors but conceptually right-wins-by-key is the model. This is why, when you write launch(Dispatchers.IO) inside a scope, the launched coroutine's context = parentContext + Dispatchers.IO + a fresh child Job: the explicitly passed elements override inherited ones, and the child always gets its own Job rather than reusing the parent's.

code

kotlin · 14 lines
kotlin
import kotlinx.coroutines.*
import kotlin.coroutines.*

fun main() = runBlocking(CoroutineName("parent")) {
    // child inherits parent's name unless overridden
    launch(Dispatchers.IO) {
        println(coroutineContext[CoroutineName])      // CoroutineName(parent) — inherited
        println(coroutineContext[ContinuationInterceptor]) // Dispatchers.IO — overridden
    }.join()

    launch(CoroutineName("child")) {
        println(coroutineContext[CoroutineName])      // CoroutineName(child) — overridden
    }.join()
}

go deeper

for a junior

Knows + merges contexts and that you pass dispatcher/name to a builder.

for a middle

States right-wins-by-key, that distinct keys coexist, and that unspecified elements are inherited.

for a senior

Explains newContext = parent + passed + fresh Job and why passing a Job manually breaks structured concurrency.

for a principal

Reasons about fold-based plus implementation, identity element, and override semantics when designing context-passing APIs.

## What + does `plus` (`+`) on `CoroutineContext` builds a **new** combined context. It never mutates either operand — contexts are immutable. The result behaves as the **union of elements, with same-key collisions resolved in favor of the right operand**. ```kotlin val ctx = Dispatchers.Default + CoroutineName("job") + Dispatchers.IO // dispatcher key appears twice -> the rightmost (IO) wins println(ctx[ContinuationInterceptor]) // Dispatchers.IO println(ctx[CoroutineName]) // CoroutineName(job) ``` ## Right-wins-by-key Matching is by **Key identity**, not by value. Two `CoroutineName`s collide because they share `CoroutineName.Key`; two different dispatchers collide because both use the `ContinuationInterceptor.Key`. The right side replaces the left for that key. Elements with *distinct* keys all coexist. ## Identity element `EmptyCoroutineContext` is the neutral element: ```kotlin val same = (Dispatchers.IO + EmptyCoroutineContext) // == Dispatchers.IO ``` This makes it the safe default when you have "no context to add." ## How coroutine builders use + When you call a builder, the new coroutine's context is computed roughly as: ``` newContext = parentContext + passedContext + freshChildJob ``` - **parentContext** is inherited from the surrounding `CoroutineScope`. - **passedContext** are the elements you hand to `launch(...)`/`async(...)`/`withContext(...)`; because they're added on the right, they **override** inherited ones (e.g. switching the dispatcher). - **freshChildJob**: the builder installs a *new* `Job` that becomes a child of the parent's `Job`. So the child never reuses the parent's exact `Job`; passing a `Job` explicitly is usually a mistake (`withContext(Job())` breaks structured concurrency). ## Practical implications ```kotlin withContext(Dispatchers.IO) { /* runs on IO, keeps the rest of the context */ } withContext(CoroutineName("db")) { /* only the name changes */ } ``` Because unspecified elements are **inherited** (left side preserved), `withContext` is a precise, surgical override of just the elements you name. ## Folding Under the hood `plus` is implemented via `fold`, removing any element from the left whose key the right side already holds, then prepending the remainder — guaranteeing the one-element-per-key invariant.

  • If you write A + B and both define a dispatcher, which dispatcher runs?
    B's dispatcher — the right operand overrides the left for the same key.
  • Does the child coroutine reuse the parent's Job?
    No. The builder installs a fresh child Job that is a child of the parent's Job, preserving structured concurrency.

Like layering CSS: the last declaration for the same property wins, while unrelated properties from earlier layers stay.

saying these in an interview costs you the question

  • Saying left operand wins on collision
  • Believing + mutates one of the operands
  • Thinking unspecified elements are dropped rather than inherited
  • Claiming the child reuses the exact parent Job
  • Not recognizing EmptyCoroutineContext as the identity

context