Kotlin Coroutines Interview Guide

Q1. What is the difference between blocking a thread and suspending a coroutine?
Answer
Blocking a thread means the thread is occupied and cannot perform any other work until the current operation finishes. Even if the task is simply waiting (for example, waiting for a network response), the thread remains unavailable.
Suspending a coroutine is different. The coroutine pauses its execution, stores its execution state in a Continuation, and releases the underlying thread back to the dispatcher. That thread can immediately execute other coroutines. When the suspended operation completes, the dispatcher resumes the coroutine on any available thread.
Q2. Can a suspended coroutine resume on a different thread?
Answer
Yes.
When a coroutine suspends, it releases its current thread. Later, when it resumes, the dispatcher schedules it on whichever suitable thread is available. Unless the dispatcher enforces thread affinity (such as Dispatchers.Main), the coroutine does not need to resume on the same thread.
Q3. What information does a Continuation store?
Answer
A Continuation stores everything required to resume execution:
Current execution state
Current label (state machine position)
Local variables that are required after the suspension point
Result or exception needed to continue execution
The compiler only stores variables that are actually needed after resumption, making continuations memory efficient.
Q4. Does every suspend function create a state machine?
Answer
No.
A suspend function only generates a compiler-created state machine when it contains at least one suspension point such as:
delay()await()withContext()yield()receive()collect()
If there are no suspension points, the compiler usually doesn't need to generate multiple states.
Q5. Does a coroutine always have a thread?
Answer
No.
A coroutine only owns a thread while it is actively executing.
When suspended, it has no thread attached to it. Only its continuation exists in memory. When the coroutine resumes, the dispatcher assigns an available thread.
Q6. When should you use launch, async and withContext?
launch
Use when you want to start independent work and don't need a result.
Example:
Fire-and-forget analytics
Logging
Background cleanup
async
Use when the coroutine produces a result that will be consumed later.
The result is represented by a Deferred<T> and retrieved using await().
withContext
Use when you want the same coroutine to temporarily execute on another dispatcher and continue only after that work completes.
Unlike launch, it does not create a new coroutine.
Q7. What are the possible outputs?
launch {
println("A")
}
println("B")
Answer
Possible outputs:
AB
or
BA
launch starts another coroutine, so scheduling determines which print executes first.
Q8. Will async block execution?
val result = async {
delay(1000)
10
}
println("Done")
Answer
No.
async immediately returns a Deferred. Since await() is never called, "Done" prints immediately.
Q9. Why doesn't this print 10?
val result = async {
10
}
println(result)
Answer
Because result is a Deferred<Int>, not an Int.
The value is obtained only after calling:
result.await()
Q10. Why is await() a suspend function?
Answer
Because if the async computation hasn't completed, the current coroutine must suspend until the result becomes available.
It suspends the coroutine—not the thread.
Q11. What is CoroutineContext?
Answer
CoroutineContext is the execution environment of a coroutine.
It contains everything required for coroutine execution:
Job
Dispatcher
CoroutineName
CoroutineExceptionHandler
It determines how and where the coroutine executes.
Q12. Which dispatcher is used?
Dispatchers.IO +
CoroutineName("Sync") +
Dispatchers.Default
Answer
Dispatchers.Default
When two elements with the same key exist, the last one replaces the previous one.
Q13. Where does a child coroutine get its dispatcher?
Answer
If a child coroutine doesn't specify a dispatcher, it inherits its parent's dispatcher through CoroutineContext inheritance.
Q14. Does a child share the same Job?
Answer
No.
Each coroutine has its own Job.
The child Job becomes a child of the parent's Job, creating a hierarchy.
Q15. Why is CoroutineContext a typed map?
Answer
Instead of using Map<String, Any>, Kotlin uses typed keys.
Advantages:
Compile-time type safety
No string typos
No unsafe casting
Easy extensibility for new context elements
Example:
coroutineContext[Job]
always returns a Job?.
Q16. Explain Structured Concurrency.
Answer
Structured concurrency means every coroutine belongs to a parent scope.
The parent controls:
lifecycle
cancellation
exception propagation
No coroutine should outlive the scope that created it.
Q17. What happens if Child B throws an exception?
Parent
├── Child A
├── Child B
└── Child C
Answer
Sequence:
Child B throws an exception.
Parent receives the exception.
Parent cancels Child A.
Parent cancels Child C.
Parent completes exceptionally.
Q18. Why do exceptions propagate upward but cancellation propagates downward?
Answer
Exceptions represent failures that must be reported to the parent so it can decide how to handle them.
Cancellation is a control signal from parent to children requesting them to stop.
Q19. When should SupervisorJob be used?
Answer
Use SupervisorJob when child coroutines are independent.
Example:
Loading a homepage:
Profile API
Wallet API
Offers API
If Offers fails, Profile and Wallet should still load successfully.
Q20. What happens when a SupervisorJob is cancelled?
Answer
Its children are also cancelled.
SupervisorJob only changes exception propagation from child to siblings. Parent cancellation still propagates downward.
Q21. Why do we have Dispatchers.Default and Dispatchers.IO?
Answer
Dispatchers.Default is optimized for CPU-intensive work.
Dispatchers.IO is optimized for tasks that spend most of their time waiting (network, disk, database).
Separating them prevents CPU-bound tasks from starving IO work.
Q22. Why doesn't Default create 100 threads on an 8-core CPU?
Answer
Only about eight CPU-bound threads can execute simultaneously on an 8-core machine.
Creating many more threads increases context switching without improving throughput.
Therefore, Dispatchers.Default keeps its thread count close to the available CPU cores.
Q23. 1000 coroutines call delay(). How many threads are blocked?
Answer
Very few (or none because of delay() itself).
delay() suspends the coroutine, stores its continuation, and releases the thread back to the dispatcher.
The waiting coroutines exist as continuations in memory rather than occupying threads.
Q24. Why can Dispatchers.IO have more threads?
Answer
IO tasks spend much of their lifetime waiting.
Additional threads improve throughput because while one thread waits for IO, another can execute useful work.
Q25. Can an IO coroutine resume on another IO thread?
Answer
Yes.
After suspension, the dispatcher schedules the coroutine on any available worker thread.
Q26. launch vs withContext
launch
Creates a new coroutine.
withContext
Does not create a new coroutine.
It simply switches the current coroutine's dispatcher temporarily.
Q27. How many coroutines are created?
launch {
withContext(IO) {}
withContext(Default) {}
}
Answer
One.
Only launch creates a coroutine.
Both withContext calls reuse the same coroutine.
Q28. Why is withContext a suspend function?
Answer
Because the current coroutine must pause while it switches execution to another dispatcher and then resume once the block finishes.
Q29. Which thread executes after withContext?
viewModelScope.launch {
withContext(IO) {}
println()
}
Answer
The original dispatcher.
withContext restores the previous context before continuing execution.
Q30. Why might job.cancel() not stop a CPU loop?
Answer
Cancellation in coroutines is cooperative.
If the coroutine never reaches a suspension point or checks cancellation using ensureActive(), yield(), or isActive, it continues executing.
Q31. Why do we need ensureActive(), yield() and isActive?
Answer
Because coroutine cancellation is cooperative rather than forceful.
These APIs provide explicit points where the coroutine checks whether cancellation has been requested.
Q32. Where should ensureActive() be placed?
Answer
Inside long-running loops or expensive computations so cancellation is detected promptly.
Q33. Homepage APIs: Profile, Wallet, Offers. One fails.
Answer
Use supervisorScope or SupervisorJob.
These APIs are independent and should not cancel one another.
Q34. Money Transfer: Debit, Credit, Update Ledger.
Answer
Do not use SupervisorJob.
These operations are dependent and must succeed together.
Failure should cancel the entire operation.
Q35. What's wrong here?
viewModelScope.launch {
launch(IO) {
val user = api.getUser()
}
renderUI()
}
Answer
renderUI() doesn't wait for getUser().
The child coroutine executes independently, causing a race condition.
Q36. When are callbacks still useful?
Answer
Callbacks are naturally suited for continuous event streams such as:
BLE events
MQTT messages
Location updates
Sensor data
Button clicks
These represent ongoing events rather than a single result.
Q37. What does the compiler generate for a suspend function?
Answer
Conceptually:
fun login(
continuation: Continuation<Unit>
): Any?
The compiler transforms the function into a state machine.
Each suspension point becomes a state identified by labels, and execution resumes using the continuation.
Q38. Why doesn't Continuation store every variable?
Answer
The compiler performs analysis and stores only variables required after suspension.
Unused variables become eligible for garbage collection.
Q39. Where is execution state stored?
Answer
Inside the Continuation object.
Q40. Who decides which thread executes a resumed coroutine?
Answer
The Dispatcher.
Q41. Why can't a normal function call a suspend function?
Answer
Suspend functions require a Continuation so they can suspend and later resume execution.
Normal functions have no continuation and therefore cannot support suspension.
Q42. Why doesn't delay() block a thread?
Answer
delay() registers a timer, suspends the coroutine, stores its continuation, and immediately releases the thread.
When the timer expires, the dispatcher resumes the continuation.
Q43. Can multiple coroutines execute on the same thread?
Answer
Yes.
Different coroutines can execute on the same thread at different times.
The dispatcher schedules them as threads become available.
Q44. Can one coroutine execute on multiple threads?
Answer
Yes.
After each suspension, the dispatcher may resume the coroutine on a different thread.
Q45. What is the biggest misconception about coroutines?
Answer
The biggest misconception is that a coroutine is a lightweight thread.
A coroutine is not a thread.
It is a unit of work whose execution is scheduled by a dispatcher. It only occupies a thread while actively executing and releases it whenever it suspends.
Comments (0)
No comments yet. Be the first to share your thoughts!
Leave a Comment
Get the latest updates
Join our newsletter and never miss an article.