Skip to content

kotlinx.coroutines

Networking & ConcurrencyConcurrency/ParallelismKotlin

What it is

The coroutine runtime for Kotlin, providing structured concurrency, dispatchers, channels and Flow for asynchronous and reactive programming.

suspend functions can pause without blocking a thread. Scopes bound their children's lifetime, dispatchers choose the thread pool, and Flow models streams of values.

Installation

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")

Getting started

The smallest useful thing you can do with it, and what each part means.

Structured concurrency
suspend fun loadDashboard(userId: String): Dashboard = coroutineScope {
    // Both start now and run concurrently.
    val profile = async { api.fetchProfile(userId) }
    val orders  = async { api.fetchOrders(userId) }

    Dashboard(profile.await(), orders.await())
}
// coroutineScope does not return until both finish. If either throws,
// the other is cancelled and the exception propagates.

viewModelScope.launch {
    val dashboard = withTimeout(5_000) { loadDashboard("u-1") }
    render(dashboard)
}
This is what structured means: there is no way to leave a coroutine running after the function returns, so background work cannot leak past the screen that wanted it.
Dispatchers
// CPU-bound work, sized to core count.
val result = withContext(Dispatchers.Default) { heavyComputation() }

// Blocking I/O — JDBC, file access, legacy libraries.
val rows = withContext(Dispatchers.IO) { jdbcTemplate.query(sql) }

// UI updates (Android).
withContext(Dispatchers.Main) { textView.text = result }
Calling a blocking JDBC method on Dispatchers.Default starves the shared pool used by every other computation. Wrapping it in Dispatchers.IO is the fix.

Advanced usage

Where the library earns its place over a simpler alternative.

Flow for streams
fun searchBooks(query: Flow<String>): Flow<List<Book>> = query
    .debounce(300)                    // wait for typing to settle
    .filter { it.length >= 2 }
    .distinctUntilChanged()
    .flatMapLatest { term ->          // cancel the in-flight search
        flow { emit(api.search(term)) }
            .catch { emit(emptyList()) }
    }
    .flowOn(Dispatchers.IO)

// Hot state that survives collectors coming and going.
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state.asStateFlow()
flatMapLatest is what makes a search-as-you-type correct: each new term cancels the previous request, so a slow earlier response cannot overwrite a newer one.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

Work continues after the screen closes
It was launched in GlobalScope or an unbound scope. Use viewModelScope, lifecycleScope, or a scope you cancel.
A coroutine silently stops working
A catch (e: Exception) swallowed CancellationException. Catch specific exceptions, or rethrow cancellation explicitly.

Best practices

  • Never use GlobalScope — work launched there outlives the component and leaks.
  • Wrap blocking calls in withContext(Dispatchers.IO); do not block Default or Main.
  • Never catch CancellationException with a blanket catch — rethrow it.
  • Expose StateFlow rather than MutableStateFlow so callers cannot mutate your state.

Background

Why it exists, and what it was reacting to.

Coroutines are a language feature, but the scopes, dispatchers and Flow API that make them usable live in this library. Its defining idea is structured concurrency: a coroutine cannot outlive the scope that started it.