Published on October 17, 2025
Kotlin’s coroutines provide a powerful framework for writing asynchronous code in a sequential style. At the heart of this framework are suspending functions — functions marked with the suspend keyword that can pause and resume without blocking the underlying thread. This means you can write code that looks synchronous, yet under the hood it yields control (freeing the CPU) during long-running operations like network calls or delays. In practical terms, a suspend fun is like a bookmark in a story: the function can leave off at one point (let other work run), then later resume exactly where it left off, preserving its state.
Put simply, a suspend function is special: it can “pause its execution and save its state at a certain point”. Unlike a normal function which runs start-to-finish on one thread, a suspend function may yield at suspension points and let other coroutines run in the meantime, all without blocking the thread. As the Kotlin docs note, a coroutine (the “suspendable computation”) lets you write concurrent code in a clear, sequential style. In practice, calling a suspend function does not create a new thread — it simply marks that this function can suspend.
suspend fun fetchData(): String {
delay(1000L) // Simulates a long-running I/O or network call
return "Data fetched!"
}
In the example above, calling fetchData() will pause for 1 second (releasing the thread) before resuming and returning the result. The delay(1000L) function is itself a suspend function that does not block the thread.
To declare a suspending function, simply prefix it with suspend:
suspend fun myTask() {
println("Starting task")
delay(500) // pause here
println("Task resumed")
}
According to the documentation, “the most basic building block of coroutines is the suspending function” — it allows the code to pause and resume later, without changing how you structure your code.
However, there are some rules for calling suspend functions. You cannot call a suspend function from a regular, non-suspending function. Instead, calls to suspend functions must occur from within another suspend function or a coroutine. For example, you can use runBlocking in a main() function to start a coroutine and call suspend code synchronously:
fun main() = runBlocking {
// runBlocking starts a coroutine and blocks the current thread until completion
val result = fetchData() // fetchData() is suspend
println(result) // This will print "Data fetched!" after ~1 second
}
Here runBlocking creates a coroutine scope on the main thread and blocks it until the coroutine finishes. This is useful for small programs or tests, but in production code (e.g. Android apps) you’d typically launch coroutines without blocking the thread. In any case, the key point is: marking a function with suspend tells the Kotlin compiler it may suspend its execution, and therefore it can only be entered from a coroutine context.
To actually start a coroutine (and thereby call suspend functions), you need a coroutine scope and a builder. Think of a scope as a container that manages the lifecycle of coroutines. Kotlin provides several coroutine builders:
launch { ... } – starts a new coroutine without blocking the current thread. It returns a Job handle (which you can use to cancel or join). It’s great for “fire-and-forget” tasks.
async { ... } – similar to launch, but returns a Deferred which is like a promise of a result. You call .await() on it to suspend until the result is ready.
runBlocking { ... } – starts a coroutine and blocks the current thread until it’s done. Use this only when necessary, since it blocks.
For example:
fun main() = runBlocking {
// Launch a background job
launch {
println("Launched coroutine")
delay(500)
println("Coroutine done")
}
// Async job returning a result
val deferredResult = async {
delay(300)
"Hello from async"
}
println(deferredResult.await()) // await() waits for the result
// runBlocking will wait for both launched coroutines to finish
}
Here, launch { ... } starts a coroutine that does some work, and async { ... } starts another that returns a String. Calling await() suspends until that result is ready. Because we are in runBlocking, the main thread is suspended until all child coroutines complete.
Quick Summary of Builders: launch { } for fire-and-forget, async { } for tasks with results, and runBlocking { } to bridge suspending code into a non-suspending context.
It helps to know how suspend actually works internally. When you write a suspend function, the Kotlin compiler rewrites it using Continuation-Passing Style (CPS). Conceptually, it turns your function into a state machine that can pause and resume. At each suspension point (like delay() or other suspend calls), the function’s current state is captured in a Continuation object. This continuation holds the local variables and “where to go next”. When the suspend function resumes, the continuation resumes execution from that point.
Analogy: Imagine each suspend function as a recipe with bookmarks. When it hits a suspension point, it places a bookmark, and the thread can do other recipes (tasks). When the suspended operation (e.g. I/O) completes, the bookmark tells the recipe where to pick up again.
For example, the compiler generates code roughly like this behind the scenes: it creates a when statement (state machine) that steps through each suspension point. Each call to a suspend function gets an extra hidden parameter – the continuation – which the compiler manages. The good news is that this happens automatically for you, so you don’t have to write any of that machinery. You just write your suspend function normally, and Kotlin handles the rest.
One important consequence is that suspension doesn’t consume a thread. When a coroutine suspends, the underlying thread is free to run something else. Later, the continuation will resume (perhaps on the same or another thread, depending on the dispatcher). For example, Android’s docs emphasize that suspension “doesn’t block the thread where the coroutine is running. Suspending saves memory over blocking while supporting many concurrent operations”. In fact, millions of coroutines can share a pool of threads because of this lightweight suspension model.
A coroutine context (especially the CoroutineDispatcher) controls what thread or threads a coroutine runs on. By default, coroutines inherit the dispatcher from their scope. You can override this with builders or with withContext(). For example, Dispatchers.IO is optimized for blocking I/O (like network or disk), while Dispatchers.Main (in Android) runs on the main thread for UI work.
You switch context using functions like withContext(), which is itself a suspend function. For instance:
suspend fun fetchOnIO(): String = withContext(Dispatchers.IO) {
// This block runs on an IO-optimized thread pool
delay(1000L) // simulate network delay
"Result from IO"
}
Here, withContext(Dispatchers.IO) { ... } suspends the current coroutine, switches to an IO thread, executes the block, then switches back to the original context. The coroutine context manages which thread or dispatcher the coroutine should run on. Dispatchers.IO can be used for offloading tasks to a background thread, while Dispatchers.Main is used for UI updates. In practice, you’ll often use withContext or pass a dispatcher to launch/async to make sure heavy work doesn’t run on the main thread.
Read More on Kotlin Coroutines: Mastering Kotlin Coroutines: A Friendly, Deep Dive Into Concurrency with Real-World Insights Kotlin Coroutines: Lightweight Concurrency for Modern Androidmedium.com
Because suspend functions can call other suspend functions naturally, you can compose complex asynchronous workflows in a linear style. For example, you might have:
suspend fun processData(): String {
val data = fetchData() // suspends until fetchData() returns
return "Processed $data"
}
This looks like normal code, but under the hood each call to a suspend function may pause the coroutine. To run multiple tasks in parallel, you can use async in the same scope. For example:
suspend fun fetchTwoDataSources(): String = coroutineScope {
val first = async { fetchDataFromNetwork() }
val second = async { fetchDataFromDatabase() }
// Both async coroutines start immediately in parallel.
"${first.await()} and ${second.await()}"
}
Each async launches a child coroutine. Calling first.await() and second.await() suspends until each result is ready. This way, the network and database fetch happen concurrently, improving performance.
Pro tip: Remember that every async should have a corresponding await (or else it’s just a fire-and-forget job). Also, use coroutineScope { ... } to automatically wait for all launched children; structured concurrency ensures your parent coroutine won’t finish until its children do.
Error handling in coroutines is similar to normal code. You can use try/catch inside suspend functions. For example, to catch exceptions during a fetch:
suspend fun fetchDataSafe(): String {
return try {
fetchData() // may throw an exception
} catch (e: Exception) {
"Error: ${e.message}"
}
}
This simply catches any exception thrown by fetchData() and returns an error string. Because coroutines propagate exceptions through their scope, an uncaught exception will cancel the parent job. You can also install a CoroutineExceptionHandler or use supervisors for more control, but the basic idea is that exceptions bubble up if not caught.
Cancellation is cooperative. You can cancel a coroutine via its Job (for example, by calling job.cancel()), and many suspend functions (like delay(), withContext, or I/O functions) are cancellable – they check for cancellation and throw a CancellationException if needed. For instance:
val job = GlobalScope.launch {
repeat(1000) { i ->
println("Working... $i")
delay(500)
}
}
delay(2000L) // let it work for a bit
job.cancel() // cancels the coroutine
println("Coroutine cancelled")
Here, after 2 seconds we cancel the job, and the coroutine stops at the next suspension point. It’s important to structure your code so that cancellation can be detected (e.g. using suspend functions or checking isActive in loops). The Android docs even list “built-in cancellation support” as a key feature of coroutines.
Kotlin enforces structured concurrency: coroutines launched in a given scope form a hierarchy of parent/child jobs. A parent coroutine will automatically wait for its children to complete, and if a parent is cancelled or fails, all its children are cancelled too. This prevents runaway coroutines or resource leaks. For example:
suspend fun loadAll() = coroutineScope {
// This coroutineScope will wait until both children finish
launch { fetchDataFromNetwork() }
launch { fetchDataFromDatabase() }
} // If either child throws an exception, both are cancelled.
By keeping coroutines “structured” in known scopes (such as tied to a lifecycle or to an outer coroutine), you ensure your asynchronous work is predictable and cleaned up properly. As the docs summarize: “coroutines form a tree hierarchy of parent and child tasks with linked lifecycles. A parent coroutine waits for its children… If the parent coroutine fails or is canceled, all its child coroutines are recursively canceled too.”. In practical terms, always launch coroutines from a scope you control (like runBlocking, coroutineScope, or an Android lifecycleScope), so that their lives are properly managed.
In real apps, suspend functions make it easy to perform I/O without blocking the UI. For example, popular libraries leverage suspend functions: Retrofit can generate suspend-based APIs for network calls, so you can simply write suspend fun fetchUser(): User and call it from a coroutine. Similarly, Room (the Android database library) supports suspend DAO methods for database operations. Because these calls suspend instead of blocking, the main thread stays responsive. You might do something like:
// In a ViewModel (or any coroutine scope)
viewModelScope.launch {
val userData = withContext(Dispatchers.IO) {
api.fetchUser() // suspend network call
}
// Switch to Main and update UI
textView.text = "Hello, ${userData.name}"
}
Suspend functions make UI updates seamless: after a suspend function returns, you can just update the UI on the main thread without callbacks.
In short, suspend functions let you write clean, linear code for asynchronous tasks. You avoid callback hell or manual thread juggling. Underneath, Kotlin’s coroutine machinery (the state machine and continuations) does the heavy lifting, so your code remains readable and concise.
From basic syntax to advanced usage, suspend functions are the core of Kotlin’s coroutines story. They empower you to perform asynchronous work (networking, database, I/O, computation) in a non-blocking way, all while keeping your code straightforward. As the official coroutine docs emphasize, coroutines are lightweight “suspendable computations”, and suspending functions are their building blocks. By mastering suspend, launch, async, and related concepts like dispatchers and structured concurrency, you unlock a modern approach to concurrency that is both efficient and easy to reason about.
Whether you’re a beginner or a seasoned Kotlin dev, understanding suspend functions will help you write more responsive and robust applications. For more information, see the Kotlin coroutine documentation and Android’s coroutines guide.