Suspend function is a function with the suspend modifier that can pause its execution without blocking a thread and resume later in the same coroutine. According to JetBrains Kotlin Docs, 2025, suspend functions are a fundamental building block of coroutines, providing asynchrony without callbacks. Each suspend function is compiled into a state machine based on Continuation, allowing efficient management of suspension points.
Key Takeaways
Suspend function is a function declared with the suspend keyword that can pause execution at one or more points without blocking the thread. Each call to a suspend function within another suspend function is a potential suspension point.
suspend fun fetchUserData(): User {
val response = httpClient.get("/user")
return parser.parse(response)
}
The Kotlin compiler translates such a function into a state machine. Each suspension point (a call to another suspend function) becomes a state (label). The current thread is released between states, and after the awaited operation completes, execution resumes from the next state.
Suspend functions appeared in Kotlin 1.3 (2018) along with coroutines as an experimental feature and became stable in Kotlin 1.5 (2021). Before that, asynchrony in Kotlin/Java was achieved through callbacks, RxJava, and CompletableFuture. Suspend functions offered an alternative with linear syntax and automatic thread management.
Understanding the inner workings of suspend functions is the key to working correctly with coroutines. Unlike regular functions, each suspend function is compiled into a class with the Continuation interface.
The Kotlin compiler adds a Continuation-type parameter at the end of each suspend function's parameter list. Continuation contains:
Suppose we have a suspend function with two calls to other suspend functions:
suspend fun process() {
val a = stepOne()
val b = stepTwo(a)
println(b)
}
The compiler transforms it into a state machine with labels:
// Simplified generated code representation
fun process(cont: Continuation<Unit>): Any? {
val cont = cont as ProcessContinuation
when (cont.label) {
0 -> {
cont.label = 1
if (stepOne(cont) == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
}
1 -> {
cont.label = 2
val a = cont.result as TypeA
if (stepTwo(a, cont) == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
}
2 -> {
println(cont.result)
Unit
}
}
}
Key observation: if the function returns COROUTINE_SUSPENDED, the current thread is released. When the async operation completes, Continuation.resumeWith is called, and the state machine continues from the next label.
Declaring a suspend function is no different from a regular one, except for the suspend keyword before fun. There is only one restriction: a suspend function can only be called from a coroutine or another suspend function.
suspend fun delayAndReturn(ms: Long): String {
delay(ms)
return "Done after ${ms}ms"
}
In this example, delay is also a suspend function that suspends the coroutine for the specified number of milliseconds without blocking the thread. After the delay, execution resumes.
fun main() = runBlocking {
val result = delayAndReturn(1000)
println(result)
}
runBlocking creates a bridge between the regular world and coroutines. Inside the lambda, any suspend functions can be called.
Kotlin supports suspend versions of functional types — suspend () -> T and suspend (A) -> B. This allows passing async lambdas to higher-order functions:
suspend fun withRetry (
retries: Int = 3,
block: suspend () -> T
): T {
repeat(retries - 1) {
try { return block() }
catch (_: Exception) { delay(100) }
}
return block()
}
The withRetry function takes a suspend lambda and retries its execution on errors. This is a typical pattern for network requests with retries.
The differences between suspend and regular functions go beyond simply adding a modifier. Let's look at the key distinctions.
| Characteristic | Regular function | Suspend function |
|---|---|---|
| Execution thread | Blocks the thread until completion | Can release the thread and resume later |
| Compiler parameters | Only specified parameters | Implicit Continuation at the end |
| Call from regular function | Yes | No |
| Stack | Physical thread stack | State machine in heap + physical stack between points |
| Return value | Direct value | Value or COROUTINE_SUSPENDED |
| Performance | Minimal overhead | ~a few nanoseconds per state machine (Kotlin 1.9+) |
A regular function does not have a Continuation — it has nowhere to save state and nothing to resume execution with. If you need to call a suspend function from a regular one, use runBlocking (for tests) or CoroutineScope.launch (for production with lifecycle awareness).
Let's look at three real-world scenarios for using suspend functions in Android apps with Kotlin.
Room supports suspend functions directly — the query is executed on a background thread automatically:
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getUser(id: Int): User?
@Insert
suspend fun insertUser(user: User)
}
Room internally uses Dispatchers.IO to execute the query, and the result is returned on the dispatcher where the suspend function was called.
class ProfileViewModel : ViewModel() {
private val repo = ProfileRepository()
fun loadProfile(id: String) {
viewModelScope.launch {
val profile = repo.getProfile(id)
_profile.value = profile
}
}
}
ViewModelScope.launch creates a coroutine, inside which the getProfile suspend function is called. After getting the result, the UI is updated on the main thread.
suspend fun placeOrder(cart: Cart): OrderResult {
val validated = validateCart(cart)
val payment = processPayment(validated)
val receipt = sendReceipt(payment)
return receipt
}
Three suspend functions execute sequentially. At each step, the coroutine can suspend without blocking the thread. If any step throws an exception, the remaining steps do not execute, protecting against incorrect order states.
Even experienced Kotlin developers make mistakes when designing suspend functions. Let's look at the most common ones.
A suspend function does not automatically make code asynchronous. Thread.sleep(), InputStream.read(), and other blocking calls will still block the thread. Use withContext(Dispatchers.IO) to wrap blocking operations.
If a function doesn't call other suspend functions and doesn't perform async operations — the suspend modifier is redundant. It adds overhead for the state machine and restricts the calling context. Only make a function suspend when it actually suspends.
When a coroutine is cancelled, suspend functions throw CancellationException. Don't catch it mindlessly — you deprive the calling code of the ability to properly complete cancellation. If you need to perform a finalizing operation, use a finally block and NonCancellable.
suspend fun safeOperation() {
try {
doWork()
} finally {
withContext(NonCancellable) {
cleanup()
}
}
}
The finally block always executes, including on cancellation, and NonCancellable ensures the cleanup won't be interrupted.
You cannot call a suspend function directly from a callback without creating a coroutine. Use suspendCoroutine or suspendCancellableCoroutine to adapt callback style to coroutines.
Frequently Asked Questions
Yes, technically a suspend function can not call other suspend functions. The compiler will create a state machine with a single state (label 0). However, there is no practical benefit in such a function — it executes like a regular one but with overhead. Do not use suspend unnecessarily.
Kotlin provides kotlinx-coroutines-debug — a library with DebugProbes and coroutine tracing tools. In Android Studio starting from Arctic Fox, there is a built-in Coroutines tab in the Debugger that shows active coroutines, their state, and suspension points.
Each suspension point creates a new state in the state machine. For most applications, the overhead of one point is a few nanoseconds (Kotlin 1.9+). Only with tens of thousands of points in a loop should you consider combining operations or using sequence/flow.
In Kotlin, suspend is a function type modifier, not a return value marker (like async in C#). Any suspend function can have any parameters and return type, and its call syntactically does not differ from a regular function call — there is no await operator at the call site.
Use suspendCancellableCoroutine for adaptation. Inside, you register a callback that calls continuation.resume(), and return a cancellation token if the callback supports unsubscription. This is the standard pattern for wrapping old Android APIs.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also