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
| Aspect | Blocking | Suspending |
|---|---|---|
| Frees the thread? | ❌ No | ✅ Yes |
| CPU usage | High (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:
- Execution stops at the suspension point
- The current state is packed into a Continuation object
- The thread is released
- 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:
- Kotlin rewrites them into functions that accept a continuation.
- A state machine is generated.
- Each suspension point becomes a “yield” in the state machine.
- When a suspension happens, Kotlin returns a special marker:
COROUTINE_SUSPENDED - 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:
suspendCancellableCoroutinesuspendCoroutine
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:
suspendCoroutinesuspends the current coroutine.- The code inside the lambda receives a
Continuation. - You manually call
resume()orresumeWithException()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
suspendCoroutineorsuspendCancellableCoroutine.
