skip to content

CPS & State-Machine Compilation

The compiler adds a hidden Continuation parameter and rewrites the function body into a labeled state machine that stores locals across suspension points. Being able to sketch that transformation is the strongest possible answer to 'how do coroutines work'.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

What does the Kotlin compiler actually do to a `suspend` function under the hood? Describe the Continuation parameter it adds.

level: juniorimportance: must knowfreq 70%

answer

  1. Hidden Continuation<T> parameter appended
  2. resumeWith(Result) delivers value or exception
  3. Return type becomes Any?
  4. COROUTINE_SUSPENDED sentinel
  5. suspend calls suspend because continuation is threaded down

basics

~20 s

The compiler rewrites a suspend function to take a hidden extra parameter representing 'what to do next'. When the function pauses, it hands work back to that callback and resumes later instead of blocking a thread.

solid answer

~40 s

Kotlin compiles `suspend` functions using CPS (Continuation-Passing Style). The compiler adds one hidden parameter of type `Continuation<T>` to the signature: a `suspend fun foo(): String` effectively becomes `fun foo(c: Continuation<String>): Any?`. Instead of returning normally, the function delivers its result by calling `continuation.resumeWith(Result.success(value))`. If it must wait (a suspension point), it returns the special marker `COROUTINE_SUSPENDED` and is later resumed via the same continuation. This is why suspend functions can only be called from other suspend functions or a coroutine builder: the caller supplies the continuation. No new thread is created — the call simply returns control to the caller until the awaited work completes.

code

kotlin · 12 lines
kotlin
// Source
suspend fun greet(name: String): String {
    delay(100)
    return "Hi $name"
}

// Conceptual compiled shape
fun greet(name: String, completion: Continuation<String>): Any? {
    // ... may return COROUTINE_SUSPENDED, or eventually:
    // completion.resumeWith(Result.success("Hi $name"))
    return COROUTINE_SUSPENDED
}

go deeper

for a junior

Knows suspend doesn't block a thread and that the compiler adds a hidden continuation parameter.

for a middle

Can name Continuation, resumeWith, and that return type becomes Any? returning COROUTINE_SUSPENDED when waiting.

for a senior

Explains the fast path (no suspension) vs slow path and why suspend can only be called from suspend, tying it to the threaded-down continuation.

for a principal

Frames CPS as a compile-time-only transform with zero runtime framework, and can compare it to async/await desugaring in other languages.

## What CPS means **CPS = Continuation-Passing Style.** Normally a function *returns* a value. In CPS, a function instead receives an extra argument — a *continuation* — describing 'the rest of the computation', and it delivers its result by **calling** that continuation rather than returning. Kotlin uses CPS to implement `suspend`. The compiler transforms every `suspend` function's signature by appending one hidden parameter of type `Continuation<T>`: ```kotlin // You write: suspend fun fetch(id: Int): String // Compiler emits (conceptually): fun fetch(id: Int, completion: Continuation<String>): Any? ``` ## The Continuation interface ```kotlin public interface Continuation<in T> { public val context: CoroutineContext public fun resumeWith(result: Result<T>) } ``` - `resumeWith(Result.success(v))` delivers a successful value. - `resumeWith(Result.failure(e))` delivers an exception. - `context` carries the `CoroutineContext` (dispatcher, job, etc.). The helper extensions `resume(value)` and `resumeWithException(e)` wrap `resumeWith`. ## Return type becomes `Any?` The rewritten function returns `Any?` because it has two possible outcomes: 1. It finishes synchronously and returns the actual value (which fits in `Any?`). 2. It must wait, so it returns the sentinel **`COROUTINE_SUSPENDED`** (from `kotlin.coroutines.intrinsics`). The caller sees this marker and knows 'I'm suspended; don't expect a value yet — I'll be resumed later via my continuation.' ## Why suspend can only call suspend A `suspend` function needs a continuation to resume into. That continuation is threaded down from a **coroutine builder** (`launch`, `async`, `runBlocking`) which creates the root continuation. Ordinary functions have no continuation to pass, so they cannot call suspend functions directly. ## Key takeaways - No thread is blocked at a suspension point; control returns to the caller. - The transformation is purely a compile-time rewrite — there is no magic runtime. - `Continuation`, `resumeWith`, `COROUTINE_SUSPENDED`, and the appended parameter are the four pieces to name.

  • Why can't you call a suspend function from a regular (non-suspend) function?
    Because the suspend function needs a Continuation to resume into, and only suspend contexts or coroutine builders can supply one. A regular function has no continuation to pass.
  • What is the runtime cost of the CPS transform when a suspend function does not actually suspend?
    Almost none: it runs straight through and returns its value like a normal call; the only overhead is the extra parameter and a fast-path check, no allocation of a suspended state.

Like leaving a self-addressed envelope (the continuation) with a clerk: instead of waiting at the counter you walk away, and they mail you the result when it's ready.

saying these in an interview costs you the question

  • Claims a suspend function spawns or blocks a thread
  • Thinks suspend is just syntactic sugar for a callback you write yourself
  • Doesn't know about the hidden Continuation parameter
  • Says the return type stays the same (ignores Any?/COROUTINE_SUSPENDED)

context

open as a page

How does the compiler use a state machine and a `label` field to resume a suspend function with multiple suspension points?

level: middleimportance: must knowfreq 60%

basics

~20 s

The compiler splits the function at each pause point into numbered states. A label counter remembers which state to run next, so when the function resumes it jumps straight to the right spot instead of starting over.

open as a page

Where are a suspend function's local variables stored across a suspension, and what is the `$continuation` / `ContinuationImpl` object?

level: middleimportance: should knowfreq 45%

basics

~10 s

Locals that must survive a pause are saved as fields on a generated continuation object instead of living on the stack. That object also holds the label, so resuming restores everything needed to continue.

open as a page

Trace how an exception thrown by an awaited operation reaches the suspend function that awaited it, in terms of `resumeWith`, `Result`, and the state machine.

level: seniorimportance: should knowfreq 30%

basics

~20 s

Failures travel through the same resume channel as values. The continuation is resumed with a failed Result; when the state machine re-enters, it unwraps that Result, which re-throws the exception right at the line that was awaiting.

open as a page

A teammate claims 'every suspend call allocates objects and is expensive.' Using your knowledge of the CPS state-machine transform, when is that true and when is it false?

level: seniorimportance: should knowfreq 35%

basics

~10 s

It's only partly true. If a suspend call finishes without pausing, it runs almost like a normal function call with no extra allocation. Allocation and real cost happen only when it actually suspends.

open as a page