How does the Kotlin compiler implement a `suspend` function under the hood?
answer
- suspend -> CPS: hidden Continuation param, return Any?
- Suspension points become state-machine labels
- Locals spilled into Continuation fields on the heap
- COROUTINE_SUSPENDED sentinel frees the thread
- resumeWith re-enters at the saved label
basics
~20 sThe compiler rewrites a suspend function into a state machine. Each pause point becomes a state, and the function is given a hidden parameter that lets it remember where to continue from when it resumes.
solid answer
~50 sThe compiler transforms each `suspend` function using **Continuation-Passing Style (CPS)**. It appends a hidden last parameter of type `Continuation<T>` and changes the return type to `Any?` so it can return the sentinel `COROUTINE_SUSPENDED` when it actually suspends. The body is compiled into a **state machine**: every suspension point splits the function into a labeled state, with locals that must survive the pause saved into fields of a generated `Continuation` object (`ContinuationImpl`). When a suspending call would block, it returns `COROUTINE_SUSPENDED` up the call chain, freeing the thread; later, calling `continuation.resumeWith(Result)` re-enters the same function at the saved label. There is **no extra thread or stack per coroutine** — the whole pause/resume mechanism is plain method calls and heap-allocated state. This is why suspension is so cheap and why `suspend` functions can only be called from other `suspend` functions or builders.
code
kotlin · 16 lines// The signature transform the compiler performs (conceptual):
// suspend fun greet(name: String): String
// becomes
// fun greet(name: String, completion: Continuation<String>): Any?
// - returns the String, OR
// - returns COROUTINE_SUSPENDED and later calls completion.resumeWith(...)
import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*
suspend fun await(value: String): String =
suspendCoroutineUninterceptedOrReturn { cont ->
// We could store cont and resume later; here we resume immediately.
cont.resume(value)
COROUTINE_SUSPENDED // signals: result delivered via continuation
}go deeper
Knows suspend functions can pause and resume but may not know the mechanism.
Can say the compiler builds a state machine and that locals are saved, even if vague on Continuation/sentinel details.
Explains the CPS transform, the hidden Continuation parameter, COROUTINE_SUSPENDED, label-based states, and why suspend is callable only from suspend context.
Connects the heap-based continuation chain to debugging/stack-trace behavior and to performance (zero per-coroutine thread/stack).
## The problem CPS solves A normal function uses the **call stack** to remember where to return. But a coroutine must be able to **pause in the middle**, give the thread back, and **resume later** — possibly on a different thread. You can't keep a native stack frame parked for that. So the compiler moves the "where am I / what are my locals" information off the stack and onto the **heap**. ## Continuation-Passing Style (CPS) For every `suspend` function, the compiler: 1. Adds a hidden last parameter: `continuation: Continuation<T>`. A **Continuation** is "the rest of the computation" — an object with a `resumeWith(result: Result<T>)` method that says "here is my answer, continue from where I paused." 2. Changes the effective return type to `Any?`, because the function may either return a real value or return the special sentinel **`COROUTINE_SUSPENDED`** meaning "I paused; don't expect a value yet." So a declared `suspend fun fetch(): User` is compiled roughly to `fun fetch(cont: Continuation<User>): Any?`. ## The state machine The body is rewritten into a **state machine**. Each suspension point becomes a labeled state (`label = 0, 1, 2…`). A generated subclass of `ContinuationImpl` holds: - the current `label` (which resume point to jump to), - any **local variables** that must survive the suspension (spilled into fields), - a reference to the **completion** continuation (the caller's continuation). Conceptually: ```kotlin // You write: suspend fun load(): Data { val a = stepOne() // suspends val b = stepTwo(a) // suspends return combine(a, b) } // Compiler produces (simplified): fun load(cont: Continuation<Data>): Any? { val sm = cont as? LoadStateMachine ?: LoadStateMachine(cont) when (sm.label) { 0 -> { sm.label = 1 val r = stepOne(sm) // pass sm as continuation if (r == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED sm.a = r as A // didn't suspend, fall through } 1 -> { sm.label = 2 val r = stepTwo(sm.a, sm) if (r == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED sm.b = r as B } // ... } return combine(sm.a, sm.b) } ``` When `stepOne` actually suspends, it returns `COROUTINE_SUSPENDED`, which propagates up and frees the thread. Later something calls `sm.resumeWith(value)`, which re-invokes `load(sm)` — and because `sm.label` is now `1`, execution jumps straight to the next state. The locals `a`/`b` are read back from `sm`'s fields, not from a parked stack. ## Why the constraints exist - A `suspend` function can only be called from another `suspend` function or a coroutine builder, because the **continuation parameter** has to be threaded through — there is nowhere to get it in plain code. - Suspension is **cheap**: pausing is just returning a sentinel; resuming is just a method call. No thread, no native stack frame is parked. - This is also why a stack trace through suspending calls can look odd — the real "stack" is a chain of `Continuation` objects on the heap. ## Key APIs/keywords to name `suspend`, `Continuation<T>`, `Continuation.resumeWith`, `COROUTINE_SUSPENDED`, `ContinuationImpl`, `kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn`, CPS transform, state-machine `label`.
- Why does a suspend function's compiled return type become Any?So it can return either the real result value or the sentinel COROUTINE_SUSPENDED. The single Any? slot carries both outcomes without boxing into a wrapper type.
- Where do a coroutine's local variables live across a suspension?They are spilled into fields of the generated ContinuationImpl object on the heap, not kept on a native call stack.
Like a video game save point: each suspend point saves your progress (label + locals) to disk (heap) so you can resume exactly there later, even on a different machine (thread).
saying these in an interview costs you the question
- Claims coroutines park a real OS thread or native stack to wait
- Thinks suspend uses thread-local magic rather than a compiler CPS transform
- Cannot explain COROUTINE_SUSPENDED or the Continuation parameter
- Believes the JVM has a bytecode-level coroutine/fiber primitive doing this
- Says each suspend call allocates a new thread