Blog Infos
Author
Published
Topics
, , , ,
Published

Let’s gaze inside a Kotlin Coroutine structure and see the Finite State Machine (FSM) inside it. Suspension mechanics revealed.

1. Introduction

Kotlin coroutines have fast become a favorite tool for writing clean, maintainable asynchronous code on the JVM. Beneath the elegant syntax, suspending functions hide a powerful state machine transformation that the Kotlin compiler generates. This transformation can feel magical, so our goal here is to:

  • Introduce finite state machines (FSMs) as a conceptual tool.
  • Show how coroutines leverage “labels” and “continuations” to suspend and resume execution.
  • Illustrate these concepts with examples.

By the end of this article, you’ll see how coroutines “pause” and “resume” seamlessly at the JVM level, preserving local variables and continuing right where they left off.

2. A Primer on Finite State Machines (FSM)
Finite State Machine (FSM)

Finite State Machine (FSM) is a model from computer science that helps represent computations in a very structured way. An FSM has:

  1. finite set of states, such as STATE_0STATE_1STATE_2, and so on.
  2. Transitions between states, which define how the machine moves from one state to another based on the inputs it reads or events that occur.
  3. starting state, which is typically where the machine begins execution.
  4. (Optionally) an accepting condition or final state, if you’re using the FSM to recognize patterns.

Each suspension is a potential transition, and each block of code before or after a suspension is a distinct state.

FSMs vs. Turing Machines

By contrast, a Turing Machine is a more powerful theoretical model with an infinite tape of symbols and a read/write head that can move in both directions.

However, when Kotlin compiles your coroutines, it just needs to track a finite set of suspension points within each function. That’s where the FSM concept fits perfectly.

In short, the decompiled code is best viewed as an FSM, not a Turing Machine because the compiler is dealing with a finite number of suspend points in your function.

. . .

3. Suspending Functions and the suspend Keyword

When you see a function like:

suspend fun fetchData(): String {
    // maybe an HTTP call or a database operation
    return "some result"
}
  • it’s essentially being transformed into a function that takes an additional parameter: a Continuation<T>.
  • If the function needs to pause (i.e., wait for an external resource), it will return COROUTINE_SUSPENDED, along with a special object (often a synthetic class) that holds your local variables.
  • Later, when data arrives, the coroutine is resumed by calling something like continuation.resume(theResult), at which point the code picks up exactly where it left off.
. . .
4. Deconstructing the State Machine: How Kotlin Compiles Coroutines
4.a. Continuation-Passing Style (CPS)

In Continuation-Passing Style, instead of returning your final answer as a single atomic operation, your function can “return” multiple times:

  1. It can return early if it needs to pause.
  2. It can return again (resumed) once the data is ready.
Continuation

The continuation-passing-style, mentioned above, is all managed by a Continuation object, which acts like a bookmark. If you think of each suspend function as a short story, the continuation is a notecard that says “Here’s the last sentence you read, and here’s what happened with your characters (variables).”

4.b. Synthetic Classes and Local Variables

Local variables must persist across suspensions. If a function’s call stack is blown away on suspension, how do those variables remain in memory?

The trick: the Kotlin compiler generates a synthetic class (often extending ContinuationImpl) that contains fields for each local variable. So if you had:

suspend fun doSomething() {
    val token = getToken()
    delay(100)
    val result = fetchData(token)
    println(result)
}

then the compiler creates something like a DoSomethingStateMachine with fields for tokenresult, and an integer label.

That class is updated before and after each suspend point.

4.c. The Big Switch Statement
  • Each point where you call a suspend function is basically a state transition. The compiler sets label = 1, checks if your call returns COROUTINE_SUSPENDED, and so on.
  • Then, when the coroutine resumes, it looks at the label to know if it should jump to case 1, 2, or 3 in a switch statement.

This is precisely how you can “pause” in the middle of a function and then continue from that same spot later.

. . .
5. Meet the Tape: Using FSM Terminology to Understand Execution

When we say “FSM,” we often imagine a tape of symbols that the machine reads. In coroutines, we don’t literally have an infinite tape, but we do have a finite set of instructions and suspension points:

  1. Tape: The sequential list of suspend calls and the code between them — like stepping through instructions.
  2. Pointer: The label variable that tells you which state/case you’re in.
  3. Memory: The synthetic class fields that store local variables so they survive across suspensions.
5.a. Visual Diagram: The “Tape” of Execution

Imagine a horizontal strip that represents your function’s flow:

[ Step 1 ] - [ Step 2 ] - [ (S) Suspend Point ] - [ Step 3 ] - [ (S) Suspend Point ] - [ End ]
  • You begin at Step 1 (label 0). Once you reach the first suspension, you “park” the coroutine by saving label = 1 and returning COROUTINE_SUSPENDED.
  • Later, the runtime calls resume(), telling your code, “You can keep going from label 1.” Thus, you jump straight to Step 2’s aftermath.
. . .
6. Pointer Movement: Resuming Execution After Suspension

When an operation like delay(100) completes, the coroutine runtime calls continuation.resume(Unit). The pointer (i.e., the label) is already stored in the synthetic state machine object, so the code checks:

switch (label) {
  case 0:
    // ...
  case 1:
    // ...
  // ...
}

The correct case is executed, effectively picking up the storyline exactly where we left off. This is all thanks to storing local variables in that synthetic object.

. . .
7. Exception Handling in Coroutines

Coroutines handle exceptions using similar state-machine logic. If a suspend call is within a try/catch, the compiler ensures that if an exception occurs, it transitions to the catch block or bubbles the exception up if uncaught.

Calls like continuation.resumeWithException(error) trigger the jump to the appropriate state (the catch block), or they propagate further if there’s no catch in scope.

7.a. A Concrete Example: How Exceptions Flow Through the State Machine

 

suspend fun processData(): Int {
    println("Starting processData")
    try {
        val result = riskyApiCall()  // might throw
        return result
    } catch (e: Exception) {
        println("Caught: $e")
        return -1
    }
}

 

If riskyApiCall() throws an exception during suspension, the compiler logic calls resumeWithException(e). That triggers the jump into the catch section of your state machine, preserving the intuitive structure of try/catch even though we’re effectively “jumping” around code blocks at the bytecode level.

. . .
8. Scopes, Contexts, and Cancellation Mechanics

Kotlin coroutines also introduce the idea of structured concurrency through scopes. A scope contains a Job, and that job can be cancelled, propagating cancellation to all child coroutines.

Cancellation is cooperative in that each suspension point checks if the job is still active. If it’s not, a CancellationException is thrown, effectively halting the coroutine’s progress.

8.a. Cancellation Checks in the State Machine

The compiler injects cancellation checks into the state machine before each suspension point. When decompiled, we see the actual structure:

// Inside the state machine's invokeSuspend
if (!isActive) {
    throw CancellationException()
}

This check is inserted before each state transition in the switch statement, ensuring the coroutine can be cancelled at any suspension point.

8.b. Job Implementation

A Job in Kotlin coroutines is essentially another state machine that tracks its own states:

  • New (initial state)
  • Active (running)
  • Completing (finishing work)
  • Completed (terminal state)
  • Cancelling (processing cancellation)
  • Cancelled (terminal state)

The Job’s state transitions are atomic operations that affect all child coroutines in the scope. When a parent Job transitions to Cancelling, it triggers a cascade of state machine transitions in its children.

8.c. Scope and Context Integration

The CoroutineScope ties together the Job’s state machine with the coroutine’s state machine through the coroutineContext. When decompiled (greatly simplified and conceptual), you might see something like:

public final class ExampleKt {
    public static final Object example(Continuation<? super Unit> continuation) {
        ExampleStateMachine stateMachine = new ExampleStateMachine(continuation);
        if (JobSupport.get(continuation.getContext()).isActive()) {
            Object result = someOperation(stateMachine);
            if (result == COROUTINE_SUSPENDED) {
                return result;
            }
        } else {
            throw new JobCancellationException("Job was cancelled", null, null);
        }
        // ... rest of the state machine
    }
}

This integration ensures that cancellation checks are performed as part of the state machine’s normal operation, not as a separate concern.

8.d. Cooperative Cancellation Implementation

The reason cancellation is cooperative becomes clear when we look at the generated state machine code. The cancellation check only happens during state transitions (suspension points). In terms of our FSM model:

  • The “tape” only checks for cancellation when moving between states
  • There’s no external force that can stop the execution within a state
  • This is why CPU-intensive work should call yield() periodically, which creates an artificial suspension point and state transition where cancellation can be checked
9. Decompiled Code Examples

The following examples are simplified illustrations of how the compiler might transform your suspending functions. They’re designed to highlight the key concepts behind the state machine and continuation mechanics, rather than match the exact bytecode you would see in a real decompilation.

Compiler optimizations, different Kotlin versions, and build settings can all produce variations in the actual decompiled output.

9.a. fetchUserData() Example (Detailed State Machine)

Let’s look at a more practical example:

suspend fun fetchUserData(): UserData {
    val token = getToken()               // First suspension
    delay(100)                           // Second suspension
    val userData = fetchUser(token)      // Third suspension
    return userData
}

When decompiled (greatly simplified), we might see something conceptually like:

Object fetchUserData(Continuation<? super UserData> continuation) {
    final class FetchUserDataStateMachine extends ContinuationImpl {
        int label = 0;
        Object token;
        Object result;  // holds return values or intermediate results

        @Override
        Object invokeSuspend(Object result) {
            this.result = result;
            switch (label) {
                case 0:
                    label = 1;
                    Object tokenResult = getToken(this);
                    if (tokenResult == COROUTINE_SUSPENDED) {
                        return tokenResult;  // Pause
                    }
                    token = tokenResult;      // Store the token

                case 1:
                    label = 2;
                    Object delayResult = delay(100, this);
                    if (delayResult == COROUTINE_SUSPENDED) {
                        return delayResult;  // Pause
                    }

                case 2:
                    label = 3;
                    Object userDataResult = fetchUser(token, this);
                    if (userDataResult == COROUTINE_SUSPENDED) {
                        return userDataResult;  // Pause
                    }
                    return userDataResult;  // Final return
            }
            return Unit.INSTANCE;
        }
    }
    // ...
}

Each labeled case corresponds to a different state:

  1. Getting the token.
  2. Delaying for 100ms.
  3. Fetching user data with the token.

If any of these operations returns COROUTINE_SUSPENDED, we exit the function early, effectively “parking” execution until resume or resumeWithException is called.

9.b. example() FSM Parallels

To reinforce the FSM idea, consider:

suspend fun example() {
    val data = fetchData()    // State 0
    processData(data)         // State 1
    validateData(data)        // State 2
}

A simplified decompiled version might could conceptually be:

class ExampleStateMachine extends ContinuationImpl {
    int label = 0;  // "pointer"
    Object data;    // memory to store intermediate result

    @Override
    Object invokeSuspend(Object result) {
        switch (label) {
            case 0:
                label = 1;
                return fetchData(this);  // Could suspend

            case 1:
                data = result;
                label = 2;
                return processData(data, this);  // Could suspend

            case 2:
                label = 3;
                return validateData(data, this); // Could suspend
        }
        return Unit.INSTANCE;
    }
}
  • Tape: The sequence of calls (fetchData → processData → validateData).
  • Pointer: The label variable, initially 0, then 1, then 2, etc.
  • Memory: The data field storing the result of fetchData.

You can see how each step in the tape is associated with a state transition in the switch.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

No results found.

10. Performance and Advantages

Despite the seemingly complex transformations, coroutines are still quite efficient:

  • Low Overhead: Storing local variables in an object and switching on an integer state is typically cheaper than using multiple OS threads.
  • Structured Concurrency: Easy to cancel or manage multiple coroutines in a scope.
  • Readable Code: You write straightforward sequential code, but it executes in a non-blocking fashion.

A neat analogy is that each coroutine is a reader with their own bookmark, and they all share a small set of reading rooms (threads). When a reader hits a suspenseful cliffhanger, they place their bookmark in the book and free up the room for another reader. When they’re ready to continue, they return to the library and pick up precisely where they left off.

. . .
11. Common Pitfalls and Misconceptions
  1. Coroutines vs. Threads: A single thread can handle many coroutines by switching between them at suspension points. Don’t confuse coroutines with actual threads at the OS level.
  2. Blocking vs. Suspending: A blocking function will stop the thread, not just the coroutine. Use proper suspending equivalents when possible.
  3. Cancellation: Coroutines rely on cooperative checks at suspension points. If you do heavy work without suspending, cancellation may be delayed.
  4. Exception Propagation: Exceptions bubble up through coroutines, potentially cancelling their parent scope unless you use something like a SupervisorJob.
. . .
12. Conclusion

By now, you should have a solid understanding of how and why Kotlin coroutines compile down to a finite state machine rather than a full-blown Turing Machine. Each suspend call creates a “cliffhanger,” storing local variables and the current label in a synthetic class. When the async operation finishes, the compiler-generated code checks that label and jumps to the correct spot, resuming the narrative.

Key Takeaways
  • Coroutines map neatly to Finite State Machines because each function has a finite number of suspension points.
  • Turing Machines are more powerful theoretical constructs (with infinite tape), which we don’t need for the finite logic of suspend/resume.
  • The generated code uses a Continuation object to keep track of state, local variables, and exceptions in a big switch statement.
  • This approach allows you to write code in a synchronous-looking style, but under the covers, it’s entirely non-blocking and efficient.

Now that you’ve seen how the sausage is made, hopefully coroutines feel a bit less magical and a lot more ingenious. Keep this finite state machine model in mind next time you’re writing a suspending function, and you’ll better understand how your code pauses and resumes with minimal fuss.

. . .
Further Reading

For additional insights into how Kotlin compiles suspending functions into a state machine and how continuations power this process, check out these resources:

This article is previously published on proandroiddev.com.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE
Menu