Member-only story
Tricky Android Interview Questions: Kotlin Coroutines Edition
Other articles in this series
Tricky Android Interview Questions:
• Kotlin Coroutines Edition ← you’re here
• Flow & StateFlow Edition
• Jetpack Compose Effects Edition
• ViewModel & State Handling Edition
• Kotlin Object & Data Class Edition
This isn’t another “how to use coroutines” article.
This is for those interview moments when the question sounds simple — but answering it isn’t.
Like when you’re asked what can go into a CoroutineContext, and then immediately hit with:
What happens if you pass
Job() + Job() + Job()to aCoroutineScope?
The code is valid. But the behavior? Not what you’d expect — unless you know the internals.
This article is a collection of tricky coroutine questions — not academic puzzles, not trivia, but real examples that reveal how deep your understanding actually goes.
Whether you’re preparing for interviews or just want to challenge what you think you know, you’ll find value here.
Question 1: What happens if you pass Job() + Job() + Job() to a CoroutineScope?
Most developers know that CoroutineScope takes a CoroutineContext, and that you can combine multiple context elements using the + operator. So what happens if you chain a few Job() instances together?
The code used to compile and run with no errors. But does it actually create a scope with three jobs?
Not quite.
Why this is a trick question
In CoroutineContext, each element is identified by a unique key.Job and SupervisorJob share the same key: Job.Key.
When you combine context elements using the + operator, any element with a duplicate key replaces the previous one.
So in this case:
val scope = CoroutineScope(Job() + Job() + Job())You’re not stacking three jobs — you’re replacing one with the next.
Only the last Job() remains in the scope's context. The first two are silently discarded.
While this example is artificial, that’s exactly what makes it useful in interviews — it reveals whether you understand how CoroutineContext works beyond the basics.
Why this question exists
This isn’t about catching a syntax mistake — the code is perfectly valid.
The goal is to check whether you understand that CoroutineContext behaves more like a map than a list — where keys matter and the last one wins.
It also shows whether you’ve gone beyond surface-level usage and understand how coroutine contexts are actually structured.
What to remember
When combining elements of the same type in a
CoroutineContext, only the last one is kept.CoroutineContextbehaves like a map, not a list.
Bonus gotcha
Even if the question is rephrased — for example:
val scope = CoroutineScope(Job() + Job() + SupervisorJob())…the behavior stays the same.
SupervisorJob is still a Job under the hood and uses the same key (Job.Key).
So again, only the last element is kept — in this case, the SupervisorJob.
This kind of variation often appears in interviews to test whether you understand the principle — not just the syntax.
⚠️ Note:
In recent versions of kotlinx.coroutines, combining two Job instances using + is no longer allowed — it results in a compilation error.
This change was introduced to prevent confusion: Job + Job is meaningless, since the second one always replaces the first.
That means code like this will now fail to compile:
val scope = CoroutineScope(Job() + Job() + Job())Even though this code no longer compiles, the question still appears in interviews — not to test your memory of syntax, but to check whether you understand how CoroutineContext merging and key replacement work.
Question 2: What happens if an exception is thrown inside an async coroutine — but you never call await()?
It sounds simple: an exception is thrown, so the program should crash — right? Not quite.
Why this is a trick question
Unlike launch, where exceptions are immediately propagated, async stores exceptions inside the resulting Deferred.
If you never call await(), the exception stays hidden.
Here’s a minimal example:
val scope = CoroutineScope(Dispatchers.Default)
fun main() {
scope.async {
throw RuntimeException("Something went wrong")
}
println("Done")
Thread.sleep(1000) // Give coroutine time to run
}Output:
DoneNo crash. No logs. The coroutine failed — but no one observed it.
Why this question exists
It checks whether you understand how error handling works in coroutines — and how async behaves differently from launch.
It also reveals a common misuse: using async when you don’t need a result.
What to remember
Exceptions in
asyncare only thrown when you callawait().
If you skipawait(), the error may go completely unnoticed.
Bonus clarification
In runBlocking, the behavior is different:
fun main() = runBlocking {
async {
throw RuntimeException("Something went wrong")
}
println("Done")
}Here, the exception will be reported, because runBlocking waits for all child coroutines and rethrows any unhandled failures.
But in regular app scopes — like CoroutineScope(Dispatchers.Default) — this doesn’t happen automatically.
Bonus insight
If you don’t need the result, use launch instead.
It reports exceptions immediately and avoids silent failures.
Question 3: What are the behavioral differences between withContext(Dispatchers.IO) and launch(Dispatchers.IO)?
These two look very similar. Both use the same dispatcher. Both run code on a background thread.
withContext(Dispatchers.IO) {
// work
}
launch(Dispatchers.IO) {
// work
}So… is there a real difference? Yes — and it matters more than it seems.
Why this is a trick question
Even though the code looks similar, these two behave differently:
withContextis a suspending function. It waits for the block to complete before moving on.launchstarts a new coroutine and returns immediately. The rest of the code continues without waiting.
Here’s a minimal example:
fun main() = runBlocking {
println("Start")
launch(Dispatchers.IO) {
delay(100)
println("Inside launch")
}
withContext(Dispatchers.IO) {
delay(50)
println("Inside withContext")
}
println("End")
}Output:
Start
Inside withContext
End
Inside launchThis shows the key difference:
withContextfinishes before"End"is printed.launchruns in parallel and prints its message later.
Why this question exists
It checks whether you understand how coroutine builders affect the order of execution.
Confusing these two can lead to subtle timing bugs — especially when the code depends on when something finishes.
What to remember
withContextwaits for the block to complete.launchruns code in parallel and doesn’t wait.
UsewithContextwhen the result or timing of the code matters.
You might also like:
Enjoyed this article?
If it helped you, or made you think differently about how coroutines work — consider leaving a clap. It helps others discover the article.
If you’re into Android interviews, Kotlin internals, or enjoy exploring subtle edge cases — feel free to follow. More content like this is on the way.
Anatolii Frolov
Senior Android Developer
Writing honest, real-world Kotlin & Jetpack Compose insights.
📬 Follow me on Medium










