On this page
Developers often describe a suspend function as "a function that runs in the background". That's wrong, and the misunderstanding causes real bugs. A suspend function can run on the main thread. What makes it special is that it can pause without blocking the thread.
What suspend actually means
A suspend function can be paused at suspension points (calls to other suspend functions such as delay, a Retrofit call or withContext) and resumed later, possibly on a different thread. While it's paused, the thread it was using is free to do other work.
Two rules follow:
- Suspend functions can only be called from other suspend functions or from a coroutine (
launch,async,runTestโฆ). - Marking a function
suspenddoes not move it off the main thread. If it does blocking work, it still blocks.
How the compiler does it
The Kotlin compiler transforms each suspend function into a state machine. It adds a hidden Continuation parameter, an object holding "where to resume and with which local variables". At each suspension point, the function saves its state into the continuation and returns a special marker. When the awaited result is ready, the continuation is resumed, jumping back into the state machine. You never write this code, but knowing it exists explains why suspension is cheap: no thread is parked, only a small object.
Writing main-safe suspend functions
Convention on Android: suspend functions should be main-safe, meaning safe to call from the main thread. Callers shouldn't need to know which dispatcher to use.
class ImageRepository(
private val context: Context,
private val io: CoroutineDispatcher = Dispatchers.IO,
private val cpu: CoroutineDispatcher = Dispatchers.Default,
) {
/** Main-safe: switches to IO for disk and Default for CPU work. */
suspend fun saveThumbnail(source: Uri, name: String): File {
val bitmap = withContext(io) {
context.contentResolver.openInputStream(source).use { BitmapFactory.decodeStream(it) }
} ?: throw IOException("Could not decode image")
val thumb = withContext(cpu) { Bitmap.createScaledBitmap(bitmap, 256, 256, true) }
return withContext(io) {
File(context.filesDir, "$name.webp").also { file ->
file.outputStream().use { thumb.compress(Bitmap.CompressFormat.WEBP_LOSSY, 85, it) }
}
}
}
}Retrofit and Room suspend functions are already main-safe. Don't wrap them in withContext(Dispatchers.IO), because it adds nothing.
Sequential by default, parallel when you ask
// Sequential: second call waits for the first (correct when B needs A's result)
val user = api.getUser(id)
val orders = api.getOrders(user.accountId)
// Concurrent: independent calls in parallel
suspend fun loadDashboard(id: String): Dashboard = coroutineScope {
val profile = async { api.getProfile(id) }
val stats = async { api.getStats(id) }
Dashboard(profile.await(), stats.await()) // if one fails, the other is cancelled
}Converting callback APIs to suspend functions
Many SDKs still use callbacks. Wrap them once with suspendCancellableCoroutine, and the rest of your code stays sequential:
@SuppressLint("MissingPermission") // caller must ensure location permission is granted
suspend fun FusedLocationProviderClient.currentLocationOrNull(): Location? =
suspendCancellableCoroutine { cont ->
val cancellation = CancellationTokenSource()
getCurrentLocation(Priority.PRIORITY_BALANCED_POWER_ACCURACY, cancellation.token)
.addOnSuccessListener { location -> cont.resume(location) }
.addOnFailureListener { error -> cont.resumeWithException(error) }
// If the coroutine is cancelled (user left the screen), cancel the underlying request too.
cont.invokeOnCancellation { cancellation.cancel() }
}For Google Play services Task APIs specifically, kotlinx-coroutines-play-services already provides Task.await().
Timeouts and cancellation
val config = withTimeoutOrNull(3_000) { api.fetchRemoteConfig() } // null if it takes > 3 s
?: RemoteConfig.DEFAULTCancellation is cooperative. Suspension points check for it automatically, but CPU-heavy loops should call ensureActive(). Never swallow CancellationException in a broad catch.
Real-world examples
Login flow
Sequential suspend calls (authenticate, fetch profile, save token) read like a recipe, with one try/catch for failures.
Image picker
Decode, resize and save with withContext switches inside one main-safe function, called from viewModelScope.
Legacy SDK
Wrap the SDK's callbacks once with suspendCancellableCoroutine, so callers never see callbacks again.
Common mistakes
- Assuming
suspendmeans "background thread". - Calling blocking APIs (
Thread.sleep,File.readTexton large files, blocking SDK calls) inside suspend functions withoutwithContext. - Using
suspendCoroutineinstead ofsuspendCancellableCoroutine, which ignores cancellation and can leak work. - Resuming a continuation twice, which crashes with
IllegalStateException. Guard callbacks that may fire more than once. - Using
runBlockingto call suspend functions from app code.
Frequently asked questions
Can a suspend function return a value?
Yes. Suspend functions return values like any function. The caller simply suspends until the value is ready.
Should every repository function be suspend?
Functions that do I/O or long work should be suspend, or return Flow. Pure, fast computations don't need suspend.
How do I call a suspend function from Java?
Not directly in a convenient way. Expose a Java-friendly wrapper, for example one returning a CompletableFuture or using callbacks, from Kotlin.
Conclusion
A suspend function is a function that can pause without blocking, implemented by the compiler as a state machine with a continuation. Write them to be main-safe with withContext, run independent work in parallel with async, wrap callbacks with suspendCancellableCoroutine, and respect cancellation.
Next step: make your APIs more expressive with Extension and Higher-Order Functions.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Kotlin fluency makes everything else in Android easier, from coroutines and Flow to Compose and clean architecture. The fastest way to build that fluency is to write real Kotlin every day and get feedback on it.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.