suspend Functions
A suspend function is a Kotlin function that can pause its execution partway through and resume later, without blocking the thread it started on. You mark any function with the suspend modifier, and the compiler then allows it to call other suspend functions and to suspend at well-defined points, such as waiting on a network response or a timer. This is the language-level foundation that Kotlin’s coroutines are built on: a coroutine is a lightweight, suspendable computation, and suspend functions are the pieces of code that know how to suspend. Understanding suspend functions is the key to understanding everything else in this Coroutines section, from launch and async to structured concurrency.
Overview / How it works
The suspend modifier is a Kotlin language feature, not a third-party library feature — the basic machinery for it lives in the standard library’s kotlin.coroutines package, available even before you add a coroutines library like kotlinx.coroutines. Writing suspend fun tells the compiler two things: this function may pause its own execution at certain points, and this function may only be called from inside a coroutine or from another suspend function — never from ordinary code with neither.
Under the hood, the compiler rewrites every suspend function using a technique called continuation-passing style (CPS). A hidden extra parameter of type Continuation<T> — essentially “what to do next, and with what result” — is appended to the function’s compiled signature. Calling a suspend function doesn’t always hand back its declared type directly at the bytecode level: it either returns the actual result immediately (if the function never truly suspended) or returns a special marker meaning “I’ve paused, I’ll notify your continuation later.” The compiler also turns the function body into a state machine: each suspension point becomes a numbered state, local variables that must survive a suspension are stored in fields, and resuming the coroutine means jumping back into the state machine at the saved state with the resumed value in hand.
The payoff of this design is that suspending is fundamentally different from blocking. A blocked thread — one stuck inside Thread.sleep or waiting on a lock — is unusable for any other work until it wakes up; the operating system still schedules it, and it still occupies a slot in whatever thread pool it came from. A suspended coroutine, in contrast, gives its thread back immediately. When a function suspends while waiting on a background operation, the calling thread is free to run other code (including other coroutines) in the meantime; only when the result is ready does the coroutine’s continuation get resumed, possibly on a different thread entirely.
In real projects you rarely call the raw APIs shown in this lesson directly. Instead you use kotlinx.coroutines builders like launch, async, and runBlocking, along with suspending functions such as delay. Those builders exist precisely to start a coroutine and supply it with a Continuation for you. Because the automatic compile check for this lesson only has the plain Kotlin standard library available, the runnable examples below use the lower-level kotlin.coroutines primitives — startCoroutine, suspendCoroutine, and Continuation — that ship in the standard library itself, so every one of them compiles with nothing but kotlinc. Seeing the raw mechanism first makes the higher-level kotlinx.coroutines API much easier to understand, because launch and async are convenient wrappers around exactly this machinery.
Syntax
The general form of a suspend function looks like a normal function with one extra modifier:
suspend fun functionName(param1: Type1, param2: Type2): ReturnType {
// body may call other suspend functions
// and may suspend at defined suspension points
return someResult
}
suspend— marks the function as suspendable; it goes directly beforefun.- Parameters and return type work exactly like a regular function; there is no special nullability rule tied to
suspenditself. - The body may call any other
suspendfunction, plus any ordinary (non-suspend) function. - The function can only be invoked from a coroutine (started by a builder such as
launch,async, orrunBlocking, or by the rawstartCoroutineshown below) or from inside anothersuspendfunction.
The kotlin.coroutines package supplies the low-level building blocks used to actually start and resume a coroutine, all available in the plain standard library:
| Declaration | Purpose |
|---|---|
Continuation<T> |
Represents “what happens next” with a result of type T; exposes resumeWith(Result<T>). |
startCoroutine(completion) |
Extension on a no-argument suspend function type; begins running it and reports the outcome to completion. |
suspendCoroutine { ... } |
Suspends the current coroutine and hands you its Continuation, so you can resume it later from a callback or another thread. |
Continuation<T>.resume(value) |
Resumes a suspended coroutine successfully with value. |
Continuation<T>.resumeWithException(e) |
Resumes a suspended coroutine by throwing e at the suspension point. |
Examples
Example 1: a suspend function that never actually suspends
This is the simplest possible suspend function — and the simplest way to start one without any external library.
import kotlin.coroutines.Continuation
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.startCoroutine
suspend fun greet(): String {
return "Hello, Kotlin!"
}
fun main() {
::greet.startCoroutine(Continuation(EmptyCoroutineContext) { result ->
println(result.getOrThrow())
})
}
Output:
Hello, Kotlin!
greet() has no suspension point inside it — it just returns a value — so calling ::greet.startCoroutine(...) runs it to completion synchronously on the main thread and immediately invokes the completion Continuation with the result, which is why the line prints right away. Notice greet is referenced with ::greet rather than called as greet(): writing greet() here would invoke it immediately, which is illegal outside a suspend context (see Common Mistakes below). The reference ::greet instead has type suspend () -> String, which is exactly what startCoroutine needs as its receiver.
Example 2: a suspend function that genuinely suspends
This example makes a suspend function actually pause and resume later, using a background thread to simulate waiting on something slow.
import kotlin.coroutines.Continuation
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.startCoroutine
import kotlin.coroutines.suspendCoroutine
import java.util.concurrent.CountDownLatch
suspend fun fetchNumber(): Int = suspendCoroutine { continuation ->
Thread {
Thread.sleep(50)
continuation.resume(42)
}.start()
}
suspend fun computeAnswer(): String {
val number = fetchNumber()
return "The answer is $number"
}
fun main() {
val latch = CountDownLatch(1)
::computeAnswer.startCoroutine(Continuation(EmptyCoroutineContext) { result ->
println(result.getOrThrow())
latch.countDown()
})
println("main() keeps going while fetchNumber() suspends")
latch.await()
}
Output:
main() keeps going while fetchNumber() suspends
The answer is 42
fetchNumber() calls suspendCoroutine, which captures the current continuation and hands it to the lambda; the lambda starts a background Thread that sleeps for 50ms before calling continuation.resume(42). Because resume isn’t called synchronously inside that lambda, fetchNumber() suspends: control returns from startCoroutine back to main() immediately, which is why "main() keeps going..." prints before "The answer is 42". Fifty milliseconds later the background thread calls resume(42), which re-enters computeAnswer() exactly where it left off. The CountDownLatch only exists to keep main() alive long enough to observe the asynchronous result; real coroutine code never needs this, because runBlocking from kotlinx.coroutines does the waiting for you.
Example 3: chaining suspend functions
Suspend functions compose like ordinary sequential code, even when several real suspensions happen along the way.
import kotlin.coroutines.Continuation
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.startCoroutine
import kotlin.coroutines.suspendCoroutine
import java.util.concurrent.CountDownLatch
suspend fun fetchUserId(): Int = suspendCoroutine { continuation ->
Thread {
Thread.sleep(30)
continuation.resume(101)
}.start()
}
suspend fun fetchUserName(id: Int): String = suspendCoroutine { continuation ->
Thread {
Thread.sleep(30)
continuation.resume("user-$id")
}.start()
}
suspend fun loadUserProfile(): String {
val id = fetchUserId()
val name = fetchUserName(id)
return "Profile loaded: $name (id=$id)"
}
fun main() {
val latch = CountDownLatch(1)
::loadUserProfile.startCoroutine(Continuation(EmptyCoroutineContext) { result ->
println(result.getOrThrow())
latch.countDown()
})
latch.await()
}
Output:
Profile loaded: user-101 (id=101)
loadUserProfile() calls two suspend functions one after another: fetchUserId() suspends for 30ms, and once it resumes with 101, fetchUserName(101) suspends for another 30ms before resuming with "user-101". Notice that the code inside loadUserProfile() reads exactly like ordinary sequential code — val id = fetchUserId() followed by val name = fetchUserName(id) — even though two real suspensions happen in between. That is the main ergonomic win of suspend functions over callback-based asynchronous code: you write straight-line logic, and the compiler’s state-machine transformation handles the pausing and resuming for you.
How it works step by step
Using Example 2 as a trace, here is exactly what happens when a suspend function suspends and later resumes:
main()calls::computeAnswer.startCoroutine(completion). This creates a coroutine and begins runningcomputeAnswer()‘s body immediately, on the calling (main) thread.computeAnswer()callsfetchNumber(), another suspend function, which in turn callssuspendCoroutine { ... }.suspendCoroutinecaptures aContinuation<Int>representing “the rest offetchNumber()and everything after it incomputeAnswer()“, and passes that continuation into the lambda.- The lambda starts a new background
Threadand returns immediately without callingresume. Because no result is available yet, the whole coroutine suspends:startCoroutinereturns control tomain()without waiting. main()continues executing the next line — printing"main() keeps going..."— while the background thread is still sleeping.- After 50ms, the background thread calls
continuation.resume(42). This re-enters the coroutine’s compiler-generated state machine at the exact point it left off, with42as the return value offetchNumber(). - Execution resumes inside
computeAnswer(), which builds the string"The answer is 42"and returns it. Since nothing is left to suspend on, the coroutine completes and calls the top-level completionContinuationpassed tostartCoroutine, which prints the final result. latch.countDown()releasesmain(), which was blocked onlatch.await(), and the program exits.
Steps 4 through 7 can happen on a completely different thread than steps 1 through 3 — suspend functions don’t pin a computation to one thread the way a blocking call does. Which thread actually resumes a coroutine is controlled by whatever resumes it (here, the background Thread we created ourselves; in real kotlinx.coroutines code, a CoroutineDispatcher decides this for you).
Common Mistakes
Mistake 1: calling a suspend function from ordinary code
The single most common suspend-function error is trying to call one from a regular, non-suspend function:
suspend fun fetchData(): String {
return "data"
}
fun main() {
val result = fetchData() // ERROR: suspend function 'fetchData' should be
// called only from a coroutine or another suspend function
println(result)
}
This fails to compile, because main() here is a plain (non-suspend) function and fetchData() can only be invoked from a coroutine or from another suspend function. The usual fix in real projects is to start a coroutine that calls it, most commonly with a builder such as runBlocking from kotlinx.coroutines:
import kotlinx.coroutines.runBlocking
suspend fun fetchData(): String {
return "data"
}
fun main() = runBlocking {
val result = fetchData()
println(result)
}
(Example 1 above shows the equivalent fix using only the standard library, via startCoroutine.)
Mistake 2: blocking the thread instead of suspending
Marking a function suspend does not automatically make everything inside it non-blocking — it’s still possible to write code that blocks:
suspend fun waitAndReturn(): Int {
Thread.sleep(1000) // blocks the entire underlying thread for a full second
return 42
}
This compiles and even “works”, but it defeats the entire point of using a suspend function: Thread.sleep blocks the underlying thread for a full second, so that thread cannot run any other coroutine in the meantime — exactly the problem coroutines exist to avoid. The fix is to use a genuinely suspending operation, such as delay from kotlinx.coroutines, which pauses the coroutine and frees its thread:
import kotlinx.coroutines.delay
suspend fun waitAndReturn(): Int {
delay(1000) // suspends without blocking the underlying thread
return 42
}
Mistake 3: assuming suspend means “runs on a background thread”
Marking a function suspend says nothing about which thread it runs on — by itself it doesn’t move any work off the calling thread. Example 1’s greet() runs entirely, synchronously, on the same thread that called startCoroutine; it just happens to be eligible to suspend if it needed to. What actually determines the thread is either code you write yourself, like the background Thread in Example 2, or, in real coroutine code, the CoroutineDispatcher a coroutine is launched with (for example Dispatchers.IO or Dispatchers.Default). Don’t reach for suspend as a shortcut for “make this run in the background” — it only means “this function is allowed to pause.”
Best Practices
- Only mark a function
suspendif it genuinely suspends (calls another suspend function or a suspending API) or is meant to be called from one — don’t add it defensively “just in case”. - Keep suspend functions free of blocking calls (
Thread.sleep, blocking I/O, long-held locks); use their non-blocking, suspending equivalents instead so the calling thread stays free for other work. - Prefer the high-level
kotlinx.coroutinesbuilders (launch,async,runBlocking) over the rawstartCoroutine/suspendCoroutineprimitives shown in this lesson for everyday code — the raw APIs are worth understanding, but they mainly matter to library authors implementing new suspending primitives. - When bridging a callback-based API into a suspend function, use
suspendCoroutine(or, for cancellable APIs,suspendCancellableCoroutinefromkotlinx.coroutines) and make sureresumeorresumeWithExceptionis called exactly once. - Let exceptions propagate normally out of suspend functions with ordinary
try/catch— a thrown exception resumes the continuation viaresumeWithException, so regular exception handling in the caller still works. - Document what a suspend function actually suspends on (a network call, a timer, a lock) in its name or documentation — the word “suspend” alone doesn’t tell a caller how long it might pause or what it’s waiting for.
Practice Exercises
- Write
suspend fun greetTwice(): Stringthat calls a suspend functiongreet(): String(like the one in Example 1) twice and returns the two results joined by a space. Start it with::greetTwice.startCoroutine(...)as in the examples above. Expected output:Hello, Kotlin! Hello, Kotlin!. - In Example 2, change
continuation.resume(42)tocontinuation.resumeWithException(RuntimeException("boom"))instead. What happens when the completion lambda callsresult.getOrThrow()? Hint: a suspended coroutine’s exception is delivered to the sameContinuationthat would have received a successful result. - Explain why the following fails to compile, then fix it:
suspend fun logStep(n: Int) { println("Step $n") }called in a loop inside a plainfun main(). Hint: see Common Mistakes, Mistake 1.
Summary
- A
suspendfunction can pause and later resume its execution without blocking the thread it runs on. suspendis a Kotlin language feature backed bykotlin.coroutinesin the standard library;kotlinx.coroutinesbuilders likelaunch,async, andrunBlockingare convenient wrappers around this mechanism.- The compiler transforms every suspend function into continuation-passing style: a hidden
Continuationparameter and a state machine that can resume execution exactly where it left off. - A suspend function can only be called from a coroutine or from another suspend function — never from ordinary code.
suspendCoroutinelets you bridge a callback-based API into a suspend function by capturing itsContinuationand callingresumeorresumeWithExceptionlater.suspendsays nothing about which thread code runs on — suspending and “running in the background” are different concepts.- Avoid blocking calls inside suspend functions; use genuinely suspending operations so the calling thread stays free.
