skip to content

Explain why a `suspend () -> T` value cannot be assigned to a `() -> T` variable. What does the compiler actually generate?

level: seniorimportance: should knowfreq 35%

answer

  1. CPS: hidden `Continuation<T>` param appended
  2. Return becomes `Any?`, may be `COROUTINE_SUSPENDED`
  3. `Function0` vs `SuspendFunction0` — distinct interfaces
  4. Suspend lambda compiles to a state machine
  5. Bridge with runBlocking / suspend { }

basics

~20 s

Because they are different types under the hood. The compiler turns a suspend function into one that takes an extra hidden parameter to track where to resume, so its shape doesn't match a plain function.

solid answer

~40 s

Suspend functions are compiled with **continuation-passing style (CPS)**: the compiler appends a hidden `Continuation<T>` parameter and changes the return type to `Any?`, returning either the result or the sentinel `COROUTINE_SUSPENDED`. So `suspend () -> T` lowers to roughly `(Continuation<T>) -> Any?`, implemented as `kotlin.coroutines.SuspendFunction0<T>` rather than `Function0<T>`. Because the runtime arity and signature differ, the two function types are not subtypes of each other — you cannot assign a `suspend () -> T` to a `() -> T` or pass one where the other is expected. The suspend lambda is also compiled to a state machine (`ContinuationImpl` subclass) that stores local state across suspension points, enabling resume without blocking a thread. To bridge, you invoke inside a coroutine (`runBlocking`/`launch`) or wrap a plain function as suspend trivially (a non-suspending suspend lambda).

code

kotlin · 10 lines
kotlin
// Conceptual lowering — NOT real source you write:
// suspend fun greet(name: String): String { delay(10); return "hi $name" }
// becomes roughly:
// fun greet(name: String, cont: Continuation<String>): Any? {
//     // state machine; returns COROUTINE_SUSPENDED across delay
// }

val plain: () -> Unit = {}
// val s: suspend () -> Unit = plain  // ERROR: type mismatch
val s: suspend () -> Unit = { plain() } // OK: wrap in a suspend lambda

go deeper

for a junior

May only know the two types are incompatible without the mechanism.

for a middle

Knows they are different types and that a coroutine context is needed, but may not detail CPS.

for a senior

Explains CPS, the hidden Continuation parameter, COROUTINE_SUSPENDED, and the state-machine lowering as the root cause.

for a principal

Connects the ABI to library design (interop with Java callbacks via suspendCancellableCoroutine, performance of the state machine, inline suspend lambdas).

## The two types are genuinely different `() -> T` is represented at runtime by `kotlin.Function0<T>`; `suspend () -> T` by `kotlin.coroutines.SuspendFunction0<T>`. These are distinct interfaces, so the compiler treats them as **unrelated types** — neither is a subtype of the other. That is the type-system reason assignment fails. ## What the compiler generates: CPS + state machine Kotlin compiles `suspend` functions using **continuation-passing style (CPS)**: - A hidden parameter `Continuation<T>` is appended to the signature. - The declared return type `T` becomes `Any?` at the bytecode level; the function returns either the real value or the special marker **`COROUTINE_SUSPENDED`** when it suspends. - The body is rewritten into a **state machine**: each suspension point (each call to another suspend function) becomes a labeled state. Local variables that must survive suspension are stored as fields of a generated `ContinuationImpl` subclass. On resume, execution jumps back to the saved label. Conceptually: ```kotlin // you write suspend fun load(): String { ... } // compiler emits (roughly) fun load(cont: Continuation<String>): Any? { /* state machine; may return COROUTINE_SUSPENDED */ } ``` For a suspend **lambda** of type `suspend () -> T`, the generated class implements `SuspendFunction0<T>` whose `invoke` carries the continuation, and it also extends the coroutine state-machine base. ## Why this matters for invocation Because the continuation is supplied by the coroutine machinery, you can only invoke a suspend function value where that machinery exists — a `suspend` function or coroutine builder. There is no continuation available in plain code. ## Bridging the two - **Plain → suspend** is trivial: a lambda `{ ... }` assigned to a `suspend () -> T` parameter just never suspends. (A plain `() -> T` reference is still not directly assignable, but you can wrap: `suspend { plain() }`.) - **Suspend → plain** requires running it in a coroutine and blocking/awaiting: `runBlocking { block() }`. ## Takeaways - Non-assignability is a deliberate consequence of the CPS ABI, not an arbitrary rule. - Suspension does not block a thread; it returns `COROUTINE_SUSPENDED` and the dispatcher resumes later.

  • What is `COROUTINE_SUSPENDED` and when is it returned?
    A sentinel object returned by a suspend function (in its CPS form) when it actually suspends instead of completing immediately; the caller's state machine sees it and yields control, resuming later via the continuation.
  • Does suspending block the underlying thread?
    No. The function returns control to the dispatcher; the thread is free to run other work. Resumption happens later via the saved continuation. Blocking calls (like Thread.sleep) defeat this.

A suspend function is like a bookmarked book: the hidden Continuation is the bookmark telling it which page to resume from — a plain function has no bookmark slot.

saying these in an interview costs you the question

  • Saying suspend and plain function types are interchangeable
  • Claiming suspension blocks the thread
  • Not mentioning the hidden Continuation parameter / CPS
  • Thinking the compiler just adds a flag rather than rewriting to a state machine
  • Believing `suspend () -> T` is a subtype of `() -> T`

context