runBlocking — a Coroutine Builder in Kotlin that blocks the current thread until the passed coroutine completes. Unlike launch and async, it is not a suspend function and can be called from regular (blocking) code. According to JetBrains documentation, 2024, runBlocking serves as a bridge between the synchronous and asynchronous worlds, allowing you to launch coroutines from main functions and tests.
Key Takeaways
runBlocking is a Kotlin function that creates a new CoroutineScope and launches the passed coroutine, blocking the current thread until it fully completes. Unlike all other Coroutine Builders, runBlocking is not a suspend function and can be called from regular synchronous code. The runBlocking signature takes a CoroutineContext and a suspend block, returning a result of type T.
public fun <T> runBlocking(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> T
): T
runBlocking launches a new event-loop on the current thread. When the coroutine calls a suspend function (e.g., delay() or await()), runBlocking blocks the thread and executes other scheduled coroutines on the same thread until the suspended one resumes. This is cooperative blocking — the thread is not idle but processes other coroutines.
The internal mechanism of runBlocking is based on an event-loop: when a suspend function is called, runBlocking pauses execution of the current block and runs other coroutines from the queue. When the suspend function completes, execution resumes. This cycle continues until all coroutines are finished.
runBlocking uses its own single-threaded pool for executing coroutines. Unlike Dispatchers.IO or Default, runBlocking does not switch threads — it handles all coroutines on the current thread, interleaving their execution. This is the only builder that guarantees execution on the same thread.
fun main() {
val threadName = Thread.currentThread().getName()
println("Before runBlocking on $threadName")
val result = runBlocking {
println("Inside runBlocking on ${Thread.currentThread().getName()}")
delay(500L)
"Done"
}
println("After runBlocking: $result")
}
The output will show that all three println statements execute on the same thread. runBlocking does not switch threads but organizes cooperative multitasking within a single thread using an event-loop.
runBlocking is justified in three scenarios: the main() entry point in console applications, unit tests for suspend functions, and bridging — calling suspend code from callback-based or blocking libraries. In production Android code, using it on the main thread is strictly forbidden.
| Scenario | Applicability | Risks |
|---|---|---|
| main() of console application | Yes | None — it is the entry point, thread does not block UI |
| JUnit tests | Yes | Minimal — tests are synchronous by definition |
| Android UI thread | No | ANR, lags, interface freezing |
| Callback → Coroutine | Yes, with caution | Thread pool blocking with long operations |
For Android tests, use kotlinx-coroutines-test with TestDispatcher instead of runBlocking. This gives time control, automatic cleanup, and test isolation.
In most scenarios, runBlocking can and should be replaced with async alternatives. For Android, these are viewModelScope, lifecycleScope, or CoroutineScope with the proper dispatcher. For tests — TestCoroutineDispatcher and runTest.
// Bad: runBlocking on Android main thread
runBlocking(Dispatchers.Main) {
val result = networkApi.fetchData()
textView.setText(result)
}
// Good: lifecycleScope
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) { networkApi.fetchData() }
textView.setText(result)
}
For tests, the replacement is runTest from kotlinx-coroutines-test. It creates a TestCoroutineScope with virtual time, allowing you to test delays without real waiting. This speeds up tests and makes them deterministic.
The most common scenario is testing suspend functions. runBlocking in tests allows you to synchronously wait for a coroutine result without changing architecture. The second scenario is libraries with callback APIs, where suspend functions are called from a blocking context via runBlocking.
// Test suspend function with runBlocking
class RepositoryTest {
@Test
fun `fetchUser returns correct data`() {
val repository = UserRepository(FakeApi())
val result = runBlocking {
repository.fetchUser("123")
}
assertEquals("John", result.name)
assertEquals("john@test.com", result.email)
}
}
For bridging between callback and suspend worlds, use CompletableDeferred in combination with runBlocking instead of callbacks — this simplifies async operation chains and improves code readability.
Improper use of runBlocking is one of the common mistakes when transitioning from a blocking approach to coroutines. Main problems: calling on the Android Main thread, nesting runBlocking inside each other, using it inside async functions, and launching long-running operations via runBlocking.
Golden rule: runBlocking is a bridge, not a replacement. Use it only to connect blocking and non-blocking worlds. For all other tasks, use launch, async, or lifecycleScope.
Frequently Asked Questions
runBlocking is the only builder that is not a suspend function. It starts an event-loop on the current thread and does not return control until all coroutines are completed. launch and async return control immediately, executing the coroutine in the background.
Not recommended. ViewModel has a built-in viewModelScope which automatically manages coroutines and cancels them when destroyed. runBlocking in ViewModel blocks the thread and does not respond to lifecycle cancellation.
Use runTest from the kotlinx-coroutines-test library. It provides a TestCoroutineScope with virtual time control, automatic cancellation, and deterministic execution.
Event-loop is an event processing cycle inside runBlocking. When a coroutine suspends (e.g., delay()), the event-loop switches to executing other ready coroutines on the same thread. This creates the illusion of multitasking without thread switching.
Nested runBlocking on the same thread creates a deadlock — the outer block waits for the inner one, but the inner cannot start until the outer finishes. On different threads it is allowed but strongly discouraged due to debugging complexity.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also