Kotlin
Coroutine
Suspend Function
Asynchronous Programming
Non-Blocking Operations

How to call suspend function from another suspend function without blocking caller function?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In Kotlin coroutines, you usually call one suspend function from another by calling it directly. That does not block the thread in the normal sense, because suspension is cooperative: the coroutine pauses, but the underlying thread can be reused for other work.

Direct Calls Are Already Non-Blocking

If the outer function needs the result of the inner function before it can continue, the correct code is usually the simplest code.

kotlin
1import kotlinx.coroutines.delay
2import kotlinx.coroutines.runBlocking
3
4suspend fun fetchUserName(): String {
5    delay(300)
6    return "Mark"
7}
8
9suspend fun loadProfile(): String {
10    val name = fetchUserName()
11    return "Profile for $name"
12}
13
14fun main() = runBlocking {
15    println(loadProfile())
16}

delay suspends instead of blocking. While loadProfile() is waiting, the coroutine is paused, but the thread is not pinned doing nothing.

This is the core idea that causes confusion: "waiting for a result" inside coroutines is not automatically the same as "blocking a thread."

Suspending Is Not the Same as Blocking

A blocking call occupies the thread until it finishes. A suspending call gives control back to the dispatcher so other coroutines can run.

That means the real question is not whether one suspend function can call another. It can, and that is the normal pattern. The real question is whether the implementation inside the suspend function is coroutine-friendly.

Good coroutine-friendly code:

  • 'delay'
  • Retrofit suspend calls
  • database drivers with coroutine support
  • channel operations and flow collection

Potentially blocking code:

  • 'Thread.sleep'
  • file I/O done directly on the main dispatcher
  • synchronous HTTP clients
  • CPU-heavy loops on the wrong dispatcher

If the implementation blocks, the suspend keyword alone does not save you.

Use withContext for Blocking Work

When the inner function needs to perform real blocking I/O or expensive CPU work, switch dispatchers explicitly.

kotlin
1import kotlinx.coroutines.Dispatchers
2import kotlinx.coroutines.withContext
3import java.io.File
4
5suspend fun readConfigFile(): String = withContext(Dispatchers.IO) {
6    File("config.txt").readText()
7}
8
9suspend fun loadSettings(): String {
10    val text = readConfigFile()
11    return "Loaded ${text.length} characters"
12}

This still looks like a direct suspend-to-suspend call, but the blocking file read runs on Dispatchers.IO, which is the right place for that kind of work.

Run Independent Work Concurrently

Sometimes the goal is not merely to avoid blocking. Sometimes you want two independent suspend operations to overlap. In that case, use structured concurrency with coroutineScope and async.

kotlin
1import kotlinx.coroutines.async
2import kotlinx.coroutines.coroutineScope
3import kotlinx.coroutines.delay
4
5suspend fun fetchName(): String {
6    delay(300)
7    return "Mark"
8}
9
10suspend fun fetchRole(): String {
11    delay(300)
12    return "Admin"
13}
14
15suspend fun loadSummary(): String = coroutineScope {
16    val name = async { fetchName() }
17    val role = async { fetchRole() }
18    "${name.await()} - ${role.await()}"
19}

This does not make code "more non-blocking" than a direct call. It changes the execution pattern from sequential suspension to concurrent suspension.

What Not to Do

The biggest mistake is using runBlocking inside suspend code. runBlocking is meant to bridge ordinary blocking code into coroutines, typically in main functions or tests. Inside a coroutine, it blocks the current thread and works against the coroutine model.

Bad idea:

kotlin
suspend fun outer() {
    // Avoid wrapping coroutine code in runBlocking here.
}

Another mistake is using GlobalScope.async just to "avoid blocking." That escapes structured concurrency, weakens cancellation handling, and often makes the code harder to reason about.

Common Pitfalls

  • Confusing coroutine suspension with thread blocking.
  • Marking a function suspend even though it still performs blocking work on the wrong dispatcher.
  • Using runBlocking inside existing coroutine code.
  • Reaching for async when the logic is actually sequential and a direct call is clearer.
  • Using GlobalScope instead of coroutineScope or a lifecycle-managed scope.

Summary

  • Call one suspend function from another directly unless you specifically need concurrency.
  • Suspending waits logically for a result without necessarily blocking the thread.
  • Use withContext when the inner operation performs blocking I/O or CPU-heavy work.
  • Use async only when independent operations should run concurrently.
  • Avoid runBlocking inside suspend functions because it defeats the coroutine model.

Course illustration
Course illustration

All Rights Reserved.