runBlocking is a coroutine builder that blocks the current thread until your coroutine finishes. While it seems convenient, it can cause serious problems, especially on Android’s main thread.

How runBlocking Works

Here’s what happens inside runBlocking:

public fun <T> runBlocking(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T {
    val currentThread = Thread.currentThread()
    val contextInterceptor = context[ContinuationInterceptor]
    val eventLoop: EventLoop?
    val newContext: CoroutineContext
 
    if (contextInterceptor == null) {
        // Set up a private or thread-local event loop if no dispatcher is specified
        eventLoop = ThreadLocalEventLoop.eventLoop
        newContext = GlobalScope.newCoroutineContext(context + eventLoop)
    } else {
        // Reuse an existing event loop or context interceptor
        eventLoop = (contextInterceptor as? EventLoop)
            ?.takeIf { it.shouldBeProcessedFromContext() }
            ?: ThreadLocalEventLoop.currentOrNull()
        newContext = GlobalScope.newCoroutineContext(context)
    }
 
    // Launch a blocking coroutine
    val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine.joinBlocking()
}

The key point: runBlocking blocks the thread it runs on until your coroutine finishes. This means no other work can happen on that thread while it waits.

Why It’s Dangerous on Android

UI Freezes & ANRs

  • Android’s main thread handles UI updates and user interactions
  • Blocking it causes UI freezes
  • Long blocks can trigger ANR (Application Not Responding) crashes

The Problem with Convenience

  • It might seem easy to use runBlocking in lifecycle methods
  • But the problems it causes (freezes, ANRs) are much worse than the convenience it provides

When to Use runBlocking

1. Unit and Integration Tests

@Test
fun `when fetching data then returns success`() = runTest {
    val result = repository.fetchData()
    assertTrue(result.isSuccess)
}

Better: Use runTest instead of runBlocking - it skips delays and makes tests faster.

2. Background Worker Threads

thread {
  runBlocking {
    doLongRunningWork()
  }
}

Better: Use structured coroutines with job.join() instead.

Best Practices & Alternatives

ScenarioDon’t UseUse Instead
Android UI coderunBlocking { /*…*/ }lifecycleScope.launch { /*…*/ }
Waiting on a jobrunBlockingjob.join()
Unit testingrunBlockingrunTest
Background workrunBlockingStructured coroutine scopes

Key Takeaways

  1. Keep Code Non-Blocking

    • Use launch, async, and lifecycle-aware scopes
    • Avoid blocking the main thread
  2. Use Structured Concurrency

    • Manage coroutine lifecycles properly
    • Avoid orphaned tasks
  3. Test Efficiently

    • Use runTest instead of runBlocking
    • Skip real delays in tests

Conclusion

runBlocking is not a good solution for Android apps. It blocks threads and can cause serious problems. Instead, use proper coroutine scopes and builders to keep your app responsive and reliable.

To learn more about coroutines and Android development:

References