Suspending Functions — How They Work

Kotlin Coroutines introduced a new way of writing asynchronous code that is both expressive and safe: suspending functions. These functions are the core building block of the coroutine system. If coroutines are “tasks,” then suspend functions are the actions those tasks perform—actions that can pause and resume without blocking threads.

But many beginners ask:

  • What is a suspend function exactly?
  • How does it pause without blocking?
  • What happens under the hood?
  • Does it create threads?
  • How does Kotlin resume execution?

This article will answer all of those questions in a clear and structured way. We’ll explore what suspending means, the difference between blocking and suspending, how continuations work, what the compiler does behind the scenes, and how to build your own suspend functions—including converting callback-based APIs into suspending ones.

Let’s start.


What Is a Suspend Function?

A suspend function is a function that can pause its execution without blocking the thread, then resume later.

You write them like this:

suspend fun fetchUser(): User {
    delay(1000)
    return User("Alice")
}

A suspend function can call other suspend functions, including:

  • delay()
  • network calls (Retrofit supports suspend)
  • database operations (Room supports suspend)
  • custom suspend functions you write

Key characteristics of suspend functions

  • They do not block threads.
  • They can pause execution at suspension points.
  • They use continuations to remember state.
  • They must run inside a coroutine or another suspend function.

Why suspend functions?

Because they allow you to write asynchronous code as if it were synchronous, without:

  • callback hell
  • multiple nested lambdas
  • blocking threads
  • freezing the UI

In many ways, suspend functions are Kotlin’s version of async/await in JavaScript or .NET—but more tightly integrated into the language.


How Suspending Differs from Blocking

A common beginner misunderstanding is:

“Delay suspends. Thread.sleep blocks. Aren’t they the same?”

No. They are opposites.


Blocking

Blocking means:

  • The thread cannot do anything else.
  • CPU is wasted waiting.
  • Other tasks wait behind it.

Example: Blocking

Thread.sleep(1000)

Effects:

  • The current thread stops for 1 second.
  • On Android, calling this on the main thread will freeze the UI (ANR risk).
  • On a backend server, this wastes valuable worker threads.

Suspending

Suspending means:

  • The coroutine pauses.
  • The thread is released and reused for other work.
  • Execution will resume later from where it left off.

Example: Suspending

delay(1000)

Effects:

  • The coroutine is paused.
  • No thread is blocked.
  • When the timer ends, the coroutine is rescheduled.

Side-by-side comparison

AspectBlockingSuspending
Frees the thread?❌ No✅ Yes
CPU usageHigh (wasted)Minimal
Scalable for thousands of tasks?❌ No✅ Yes
Works on Android Main thread?❌ No✅ Yes (except long work)
Coroutines-friendly❌ No✅ Yes

This is why suspending functions matter: they let you handle thousands of tasks without creating thousands of threads.


Call Stack & Continuation

A big question:

If a suspend function pauses, what happens to the call stack?

Normally, if you pause a function mid-execution, you lose the call stack. But suspend functions work differently—they save their state in a Continuation.

This is the hardest but most important concept.


What is a continuation?

A continuation is an object that represents:

  • Where to resume execution
  • Local variables
  • The next instruction
  • Exception handlers

Think of it as a bookmark in the code.

When you suspend:

  1. Execution stops at the suspension point
  2. The current state is packed into a Continuation object
  3. The thread is released
  4. Later, the continuation resumes and restores the state

How Kotlin implements suspend functions internally

Take this simple suspend function:

suspend fun download(): String {
    val data = fetchNetwork()
    return process(data)
}

The Kotlin compiler transforms it into:

fun download(continuation: Continuation<String>): Any {
    // internal state machine
}

The compiler generates a state machine with labels like:

  • state 0 → before first suspension
  • state 1 → after resuming from fetchNetwork
  • state 2 → returning result

Why a state machine?

Because the compiler has to re-enter the function after a suspension, but from the middle of the function.
The state machine encodes this control flow.


Compiler Transformations

Suspend functions undergo a transformation during compilation:

  1. Kotlin rewrites them into functions that accept a continuation.
  2. A state machine is generated.
  3. Each suspension point becomes a “yield” in the state machine.
  4. When a suspension happens, Kotlin returns a special marker:
    COROUTINE_SUSPENDED
  5. When resumed, the state machine continues from the last label.

Visual mental model

Imagine rewriting:

suspend fun work() {
    step1()
    delay(500)
    step2()
}

into:

state = 0

if (state == 0) {
    step1()
    state = 1
    if (delay(500) suspends) return SUSPENDED
}

if (state == 1) {
    step2()
}

This is (roughly) how suspend functions behave internally.


Creating Custom Suspend Functions

The easiest way to create a custom suspend function is to use existing suspend calls:

suspend fun loadData(): Data {
    val a = fetchA()  // suspend
    val b = fetchB()  // suspend
    return combine(a, b)
}

But what if you want to write a suspend function that doesn’t use any other suspend function?

Kotlin provides suspension primitives, the most common being:

  • suspendCancellableCoroutine
  • suspendCoroutine

Let’s explore them.


1. suspendCoroutine

This function allows you to transform a callback-style API into a suspend function.

suspend fun waitForResult(): String =
    suspendCoroutine { continuation ->
        asyncOperation { result ->
            continuation.resume(result)
        }
    }

Explanation:

  • suspendCoroutine suspends the current coroutine.
  • The code inside the lambda receives a Continuation.
  • You manually call resume() or resumeWithException() later.

2. suspendCancellableCoroutine

This supports cancellation—critical on Android and backend servers.

suspend fun awaitCallback(): Int =
    suspendCancellableCoroutine { cont ->
        callbackApi.start(
            onSuccess = { value -> cont.resume(value) },
            onError = { e -> cont.resumeWithException(e) }
        )

        cont.invokeOnCancellation {
            callbackApi.cancel()
        }
    }

Why use it?

  • When the coroutine is cancelled, your callback API also gets cancelled.
  • Prevents leaks and zombie tasks.

Example: Building a Suspend Version of a Callback API

Let’s say you have a legacy callback API:

interface Api {
    fun getUser(
        id: Int,
        onSuccess: (User) -> Unit,
        onError: (Throwable) -> Unit
    )
}

You want to turn this into:

suspend fun Api.getUserSuspend(id: Int): User

Let’s build it.


Step 1: Create the suspend wrapper

suspend fun Api.getUserSuspend(id: Int): User =
    suspendCancellableCoroutine { cont ->

        getUser(
            id,
            onSuccess = { user ->
                cont.resume(user)
            },
            onError = { error ->
                cont.resumeWithException(error)
            }
        )

        // Optional: handle cancellation
        cont.invokeOnCancellation {
            // If API supports cancel
            cancelRequest(id)
        }
    }


Step 2: Usage inside coroutines

viewModelScope.launch {
    try {
        val user = api.getUserSuspend(42)
        updateUI(user)
    } catch (e: Throwable) {
        showError(e)
    }
}

This now plays perfectly with structured concurrency:

  • cancellation
  • lifecycle scopes
  • error propagation
  • dispatchers

Explaining the tricky parts

Why do we need suspendCancellableCoroutine?

Because callback APIs don’t suspend.
So we manually suspend by capturing the continuation.

Why do we call resume() inside callbacks?

Because Kotlin needs to know when to resume the suspended coroutine.

Why cancellation handling?

If the coroutine is cancelled:

  • You must cancel the underlying network request
  • Otherwise, you get leaks or CPU waste (background task continues)

This is especially important for Android when:

  • ViewModel is cleared
  • Fragment is destroyed
  • User navigates away

Conclusion

Suspend functions are at the core of Kotlin Coroutines. They allow asynchronous code to be written in a style that looks synchronous, while remaining efficient, scalable, and easy to reason about.

You learned:

  • What suspend functions are
  • Why suspending is different from blocking
  • How the call stack is transformed into continuations
  • How the compiler rewrites suspend functions into state machines
  • How to build custom suspend functions
  • How to convert callback APIs into coroutine-friendly suspend functions

Final takeaways

  • Suspend functions do not block.
  • They pause execution using continuations.
  • The compiler turns them into state machines.
  • They must be used inside coroutines or other suspend functions.
  • You can wrap old callback APIs with suspendCoroutine or suspendCancellableCoroutine.

Leave a Reply

Your email address will not be published. Required fields are marked *