Interview QuestionPractical QuestionFollow-up Questions

Suspend Function Compilation and the Coroutine State Machine

skydovesJaewoong Eum (skydoves)||12 min read

Suspend Function Compilation and the Coroutine State Machine

Kotlin suspend functions appear sequential in source code, but the compiler transforms them into state machines driven by Continuation-Passing Style (CPS). This transformation is the foundation of the entire coroutine system: it enables non-blocking suspension and resumption without threads blocking and without callbacks in the source. Understanding the compiler output, the Continuation interface, label-based state transitions, and the atomic coordination between suspension and resumption is essential for diagnosing coroutine behavior and reasoning about performance. By the end of this lesson, you will be able to:

  • Explain how the Kotlin compiler applies CPS transformation to suspend functions.
  • Describe the role of the Continuation interface and the generated ContinuationImpl subclass.
  • Trace execution through the label-based state machine, including how local variables survive suspension points.
  • Identify how COROUTINE_SUSPENDED controls whether a function actually suspends or takes the fast path.
  • Apply knowledge of invokeSuspend, intercepted(), and resumeCancellableWith to understand coroutine startup and dispatch.

CPS Transformation and the Continuation Interface

Every suspend function undergoes a Continuation-Passing Style transformation at compile time. The compiler adds a hidden Continuation parameter and changes the return type to Any?. A suspend function written as:

suspend fun fetchUser(): User {
    val response = api.getUser()
    val processed = processData(response)
    return processed
}

becomes, at the bytecode level, something equivalent to:

fun fetchUser(continuation: Continuation<User>): Any? {
    // state machine body
}

The Continuation interface itself is minimal:

public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}

It carries the CoroutineContext and a resumeWith callback that delivers a result back to the suspended coroutine. Every suspend function receives the caller's continuation as its last parameter. When the function completes, it calls resumeWith on that continuation to deliver the result upstream. When it suspends, it stores that continuation so a future callback can invoke resumeWith later.

The return type changes to Any? because the function can now return one of two things. If the function actually suspends, it returns the sentinel value COROUTINE_SUSPENDED. If the suspending operation completes synchronously (a fast path), it returns the actual result directly. The caller inspects the return value to decide whether to continue executing or yield control.

The Generated State Machine and Label Dispatch

The compiler converts the body of a suspend function into a state machine where each suspension point corresponds to a label. Consider a function with two suspension points:

suspend fun loadData(): String {
    val token = fetchToken()       // suspension point 0
    val data = fetchData(token)    // suspension point 1
    return data
}

The compiler generates a ContinuationImpl subclass that holds the mutable state for this function. The transformed code looks conceptually like:

fun loadData(completion: Continuation<String>): Any? {
    class LoadDataSM(
        completion: Continuation<String>
    ) : ContinuationImpl(completion) {
        var result: Any? = null
        var label: Int = 0
        var token: String? = null  // local variable that must survive suspension

        override fun invokeSuspend(result: Any?): Any? {
            this.result = result
            return loadData(this)  // re-enter the function
        }
    }

    val sm = completion as? LoadDataSM ?: LoadDataSM(completion)
    // continued below...

The dispatch over labels drives state transitions. The outer loop allows the fast path to advance to the next label immediately when a callee returns a result synchronously instead of suspending:

    var result = sm.result
    while (true) {
        when (sm.label) {
            0 -> {
                sm.label = 1
                result = fetchToken(sm)
                if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
            }
            1 -> {
                throwOnFailure(result)
                sm.token = result as String
                sm.label = 2
                result = fetchData(sm.token!!, sm)
                if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
            }
            2 -> {
                throwOnFailure(result)
                return result as String
            }
            else -> throw IllegalStateException("call to 'resume' before 'invoke'")
        }
    }
}

This interview continues for subscribers

Subscribe to Dove Letter for full access to exclusive interviews about Android and Kotlin development.

Become a Sponsor