Coroutines Under the Hood Quiz

KOTLIN › Coroutines

What does a suspend fun compile to on the JVM?

Answer: A method with an extra Continuation parameter, returning the value or a suspension marker

There is no runtime support for suspension. The compiler adds a Continuation parameter and widens the return type so the function can report that it suspended rather than finished.

Why can a suspend function only be called from another suspend function?

Answer: Only a suspending caller has a Continuation to pass as the hidden parameter

The hidden Continuation parameter has to come from somewhere, and only code already inside a coroutine has one. Builders like launch and runBlocking exist precisely to create that entry point.

Where is the state of a suspended coroutine stored?

Answer: On the heap, in a continuation object

Suspension is a real return, so the stack frame is gone. Keeping the state on the heap is exactly what frees the thread and makes many waiting coroutines cheap.

What is the significance of COROUTINE_SUSPENDED?

Answer: It is the marker meaning the function did not finish, enabling a fast path when it did

Returning anything other than the sentinel means the value was already available, so no continuation is stored and no dispatch happens. That fast path is why suspend is nearly free when nothing actually waits.

What does this print?

Answer: CoroutineName(worker), because a dispatcher occupies a different key

A dispatcher is stored under the ContinuationInterceptor key, so adding one replaces only the dispatcher. Composition replaces per key and keeps everything else.

How does a dispatcher actually take control of thread assignment?

Answer: It implements ContinuationInterceptor and wraps the continuation so resumption is dispatched

The wrapper turns resumeWith into a dispatch of a Runnable rather than an inline call. This is also why Unconfined is not a special case: it is simply an interceptor that declines to redirect.

What does viewModelScope.launch(Job()) { } do?

Answer: Detaches the coroutine, so clearing the ViewModel no longer cancels it

The fresh Job replaces the scope job under the same key, so the builder finds no parent to link to. The coroutine survives onCleared and typically leaks the ViewModel.

A CoroutineStart.LAZY coroutine inside coroutineScope is never started. What happens?

Answer: The scope waits forever, because it is an unfinished child

Laziness changes when the body runs, not whether the coroutine is part of the hierarchy. Structured concurrency makes the parent wait for every child, so this hangs.

What is the difference between CoroutineStart.UNDISPATCHED and Dispatchers.Unconfined?

Answer: Undispatched controls where the body begins; unconfined controls where it resumes

UNDISPATCHED is a start mode affecting the first execution on the calling thread. Unconfined is a dispatcher that declines to redirect resumptions after any suspension point.

Why prefer suspendCancellableCoroutine over suspendCoroutine?

Answer: It can be cancelled, and offers invokeOnCancellation to release the underlying resource

With suspendCoroutine a cancelled coroutine waits for the callback regardless, hanging its parent and leaking the continuation if it never fires. Resuming twice throws in both variants.

What does invokeOnCancellation do when bridging an OkHttp call?

Answer: Aborts the HTTP call so it stops consuming network after the coroutine is cancelled

Cancelling the coroutine stops your code but not the operation it started. Without this hook the request runs to completion after the screen is gone, still using radio and battery.

A suspend fun runs a tight CPU loop and is called from Dispatchers.Main. What happens?

Answer: It blocks the main thread and may drop frames or trigger an ANR

A suspend function runs on the caller dispatcher. The keyword changes how waiting works, not where code executes, and nothing enforces the non-blocking promise.

How long does this take, roughly?

Answer: The sum of both calls, because suspending calls in sequence run in sequence

Sequential suspending calls are sequential. Overlap requires async inside a scope, then awaiting both, because the keyword only makes waiting cheap rather than creating concurrency.

Why do local variables crossing a suspension point become continuation fields?

Answer: Because the stack frame is discarded when the function returns at that point

Suspension is a genuine return, so the frame goes away. This is also why a long-lived coroutine capturing a large object keeps it alive: it is a field on a heap object.

Which summary of coroutines is accurate?

Answer: Compiler-generated continuation-passing style plus a job hierarchy for lifetimes

The runtime has no notion of coroutines at all. The compiler rewrites suspending code into callbacks, structured concurrency adds the Job links, and everything still runs on ordinary threads.

What does suspendCancellableCoroutine do?

Answer: It suspends the coroutine until cont.resume() or cont.resumeWithException() is called, bridging callback APIs to suspend functions

suspendCancellableCoroutine suspends the current coroutine and gives you a CancellableContinuation. You resume it from a callback, bridging the callback world to the suspend world. invokeOnCancellation lets you clean up if the coroutine is cancelled while waiting.

Back to Coroutines Under the Hood