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.
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.
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.
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:
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
suspendeven though it still performs blocking work on the wrong dispatcher. - Using
runBlockinginside existing coroutine code. - Reaching for
asyncwhen the logic is actually sequential and a direct call is clearer. - Using
GlobalScopeinstead ofcoroutineScopeor 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
withContextwhen the inner operation performs blocking I/O or CPU-heavy work. - Use
asynconly when independent operations should run concurrently. - Avoid
runBlockinginside suspend functions because it defeats the coroutine model.

