๐ŸŸฃ Kotlin Programming

Suspend Functions Explained with Real Examples

The suspend keyword is small, but it changes how you write every asynchronous operation on Android. Here's what suspension really means, how the compiler implements it, and how to write, wrap and call suspend functions correctly.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Intermediate
On this page
  1. What suspend actually means
  2. How the compiler does it
  3. Writing main-safe suspend functions
  4. Sequential by default, parallel when you ask
  5. Converting callback APIs to suspend functions
  6. Timeouts and cancellation
  7. Real-world examples
  8. Common mistakes
  9. Frequently asked questions
  10. Conclusion

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 suspend does not move it off the main thread. If it does blocking work, it still blocks.
A suspend function pausing and resuming On the main thread, loadProfile starts, reaches api.getUser and suspends. The main thread is free and draws frames. When the network result arrives, the coroutine resumes on main and updates the UI state. Main thread timeline start loadProfile() suspended: thread free (frames, input) resume โ†’ update state Network (OkHttp threads) HTTP request in progress
Suspension hands the thread back. Resumption picks up exactly where the function left off, with local variables intact.

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.

ImageRepository.ktKotlin
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 vs concurrentKotlin
// 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:

LocationExtensions.ktKotlin
@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

TimeoutsKotlin
val config = withTimeoutOrNull(3_000) { api.fetchRemoteConfig() }   // null if it takes > 3 s
    ?: RemoteConfig.DEFAULT

Cancellation 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 suspend means "background thread".
  • Calling blocking APIs (Thread.sleep, File.readText on large files, blocking SDK calls) inside suspend functions without withContext.
  • Using suspendCoroutine instead of suspendCancellableCoroutine, which ignores cancellation and can leak work.
  • Resuming a continuation twice, which crashes with IllegalStateException. Guard callbacks that may fire more than once.
  • Using runBlocking to 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.

See all our apps โ†’

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.

Share: LinkedIn X / Twitter WhatsApp