跪拜 Guibai
← All articles
Kotlin · Android

Kotlin's select Clause Solves the Partial-Results Problem That awaitAll Can't

By RockByte ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Many real-world UIs need to show results as they arrive rather than waiting for the slowest backend. `select` is the idiomatic Kotlin coroutine primitive for that pattern, but it's underused partly because the API's stability markers are inconsistent and partly because developers reach for `Channel` or `launch` workarounds that add more concurrency machinery than necessary.

Summary

A common e-commerce shipping-quote requirement exposes the limits of `async` + `awaitAll`: three carriers are queried in parallel, but the UI must show whatever returns within 3 seconds, not wait for the slowest one. Wrapping `awaitAll` in `withTimeoutOrNull` discards already-completed results because `awaitAll` only delivers a complete list.

`select` with `onAwait` solves this by returning as soon as any single `Deferred` finishes. A loop collects results until a timeout, and if nothing arrives, a second phase waits for the first non-null quote using the same still-running `Deferred` instances. The approach keeps the code single-threaded within the calling coroutine, so a plain `MutableMap` is enough.

The same `select` expression also handles `Channel` sends and receives, and `Job` completion, making it a general-purpose multiplexer for coroutine events. Two production pitfalls stand out: a failed `Deferred` throws instead of returning null, so error-conversion rules must be explicit, and fallback waiting needs its own deadline so it doesn't hang forever.

Takeaways
`awaitAll` blocks until every `Deferred` completes; adding `withTimeoutOrNull` discards already-finished results because `awaitAll` never exposes partial lists.
`select` with `onAwait` returns the first available `Deferred` result without cancelling the others, so unfinished tasks remain usable for a second waiting phase.
A single `select` call picks exactly one ready branch; to collect multiple results, wrap it in a loop that removes completed tasks from the pending set.
Unselected tasks are not cancelled by `select`, which is why the same `Deferred` instances can be reused after a timeout.
Ordinary `select` is biased toward earlier-registered branches when multiple are ready simultaneously; use `selectUnbiased` for random selection.
`select` also works with `Channel.onReceive`, `Channel.onSend`, and `Job.onJoin`, making it a general multiplexer for coroutine events.
A `Deferred` that fails with an exception propagates that exception through `onAwait`; production code must decide which failures become null and which should cancel the scope.
Fallback waiting after a timeout needs its own deadline; otherwise a request that never completes can hang the coroutine indefinitely.
Conclusions

`select` is a stable API but sits alongside experimental and internal annotations, which creates confusion about whether it's safe for production use. The official docs still label it experimental despite the core functions being callable without opt-in flags.

Many Kotlin developers treat `select` as an obscure corner of the language, yet the pattern it solves — racing multiple async operations and processing partial results — is common in mobile and frontend work where UIs can't block on the slowest backend.

The `select`-plus-loop pattern keeps result collection single-threaded within the calling coroutine, avoiding the need for concurrent data structures that `launch`-based solutions would require.

The two-phase approach (collect within a window, then fall back to first-available) mirrors real product requirements more closely than a single timeout or a single `awaitAll`, and `select` makes it straightforward without restarting requests.

Concepts & terms
select expression
A Kotlin coroutine builder that waits for the first available result across multiple suspending operations (Deferred, Channel, Job). It picks exactly one ready branch per call and does not cancel unselected branches.
onAwait
A select clause registered on a Deferred. When that Deferred completes, the clause fires and select returns its result. If the Deferred failed, the exception propagates.
selectUnbiased
A variant of select that chooses randomly among multiple branches that are ready simultaneously, instead of preferring the one registered first.
withTimeoutOrNull
A coroutine utility that runs a block with a time limit. If the block doesn't finish in time, it cancels the block and returns null. It cannot extract partial results from an incomplete awaitAll.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗