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 before fun.
  • Parameters and return type work exactly like a regular function; there is no special nullability rule tied to suspend itself.
  • The body may call any other suspend function, plus any ordinary (non-suspend) function.
  • The function can only be invoked from a coroutine (started by a builder such as launch, async, or runBlocking, or by the raw startCoroutine shown below) or from inside another suspend function.

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:

  1. main() calls ::computeAnswer.startCoroutine(completion). This creates a coroutine and begins running computeAnswer()‘s body immediately, on the calling (main) thread.
  2. computeAnswer() calls fetchNumber(), another suspend function, which in turn calls suspendCoroutine { ... }.
  3. suspendCoroutine captures a Continuation<Int> representing “the rest of fetchNumber() and everything after it in computeAnswer()“, and passes that continuation into the lambda.
  4. The lambda starts a new background Thread and returns immediately without calling resume. Because no result is available yet, the whole coroutine suspends: startCoroutine returns control to main() without waiting.
  5. main() continues executing the next line — printing "main() keeps going..." — while the background thread is still sleeping.
  6. 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, with 42 as the return value of fetchNumber().
  7. 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 completion Continuation passed to startCoroutine, which prints the final result.
  8. latch.countDown() releases main(), which was blocked on latch.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 suspend if 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.coroutines builders (launch, async, runBlocking) over the raw startCoroutine/suspendCoroutine primitives 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, suspendCancellableCoroutine from kotlinx.coroutines) and make sure resume or resumeWithException is called exactly once.
  • Let exceptions propagate normally out of suspend functions with ordinary try/catch — a thrown exception resumes the continuation via resumeWithException, 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(): String that calls a suspend function greet(): 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) to continuation.resumeWithException(RuntimeException("boom")) instead. What happens when the completion lambda calls result.getOrThrow()? Hint: a suspended coroutine’s exception is delivered to the same Continuation that 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 plain fun main(). Hint: see Common Mistakes, Mistake 1.

Summary

  • A suspend function can pause and later resume its execution without blocking the thread it runs on.
  • suspend is a Kotlin language feature backed by kotlin.coroutines in the standard library; kotlinx.coroutines builders like launch, async, and runBlocking are convenient wrappers around this mechanism.
  • The compiler transforms every suspend function into continuation-passing style: a hidden Continuation parameter 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.
  • suspendCoroutine lets you bridge a callback-based API into a suspend function by capturing its Continuation and calling resume or resumeWithException later.
  • suspend says 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.