skip to content

How do you race two suspending operations and take the faster result using `onAwait` in `select`?

level: middleimportance: should knowfreq 38%

answer

  1. async -> Deferred; deferred.onAwait { result }
  2. select returns the first deferred to complete
  3. select does NOT cancel losers — cancel them yourself
  4. Winning deferred's exception propagates out
  5. Wrap in coroutineScope for structured safety

basics

~10 s

Start each operation with async to get a Deferred, then use select with each deferred's onAwait clause. select resumes with whichever finishes first. Remember to cancel the loser.

solid answer

~40 s

`Deferred<T>.onAwait { result -> ... }` is a `select` clause that becomes ready when that `async` computation completes; on winning it provides the result. To race, launch both with `async` inside a coroutine scope, then `select { d1.onAwait { ... }; d2.onAwait { ... } }`. The winner's result is returned. Crucially, `onAwait` only awaits — it does **not** cancel the loser, so you must cancel the slower `Deferred` yourself (or wrap the whole thing in a scope you cancel, e.g. via `coroutineScope` plus explicit `cancel()`), otherwise the loser keeps running and may leak resources. If the winning deferred completed exceptionally, `onAwait` rethrows that exception into the `select`.

code

kotlin · 12 lines
kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.selects.select

suspend fun firstOf(urls: List<String>, fetch: suspend (String) -> String): String =
    coroutineScope {
        val jobs = urls.map { url -> async { fetch(url) } }
        try {
            select { jobs.forEach { d -> d.onAwait { it } } }
        } finally {
            jobs.forEach { it.cancel() }
        }
    }

go deeper

for a junior

Knows you use async + onAwait to get whichever completes first.

for a middle

Understands losers are not auto-cancelled and that the winner's exception propagates, and cancels losers in a finally.

for a senior

Wraps the race in coroutineScope, reasons about resource cleanup, timeouts, and exception flow across winner/loser.

for a principal

Designs hedged-request strategies (latency vs cost), back-pressure, and cancellation guarantees for production racing at scale.

## Racing with `onAwait` An `async { }` returns a `Deferred<T>` — a future-like handle whose result you normally get via `await()`. Inside `select`, the clause `deferred.onAwait { result -> ... }` becomes **ready when the deferred completes**, and on winning passes the computed `result` to the lambda. ### The pattern ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.selects.select suspend fun fastest(): String = coroutineScope { val a = async { slowSource("A") } val b = async { slowSource("B") } select<String> { a.onAwait { winner -> winner } b.onAwait { winner -> winner } }.also { a.cancel(); b.cancel() // cancel both; the winner is already done, the loser is stopped } } ``` ### Why you must cancel the loser `select` only **resumes** with the first ready clause; it does **not** automatically cancel the operations behind the losing clauses. With `onAwait`, the losing `async` job keeps running in the background, wasting CPU, holding connections, or completing with an uncaught exception. Always cancel the losers — cancelling the already-completed winner is a harmless no-op, so cancelling both is simplest. Using `coroutineScope { }` adds a safety net: structured concurrency ensures children are cancelled if the scope itself is cancelled or fails, but within a successful `select` you still cancel explicitly to stop the loser promptly. ### Exceptions If the **winning** deferred completed exceptionally, `onAwait` propagates that exception out of `select`. A loser that fails after losing will not affect the `select` result but, if not cancelled, its failure may surface through the parent job — another reason to cancel losers. ### Adding a deadline Combine with `onTimeout` to bound the race: ```kotlin select<String?> { a.onAwait { it } b.onAwait { it } onTimeout(2.seconds) { null } } ``` If neither finishes in time, the timeout clause wins and you fall back — then cancel both deferreds. ## Summary - `onAwait` = ready when the `Deferred` completes. - `select` returns the first completed result; the rest keep running unless you cancel them. - Winner's exception propagates; cancel losers to avoid leaks and stray failures.

  • Does `select` cancel the losing `async` jobs automatically?
    No. `onAwait` only awaits completion; the loser keeps running until you cancel it (or the enclosing scope is cancelled).
  • What if the winning deferred completed with an exception?
    `onAwait` rethrows that exception out of the `select` expression, just as `await()` would.

Like sending two couriers and using the first parcel that arrives — but you must call back the other courier or they keep driving.

saying these in an interview costs you the question

  • Believing select cancels the losing coroutines for you
  • Forgetting to cancel losers, causing leaks or stray exceptions
  • Thinking onAwait swallows the winner's exception
  • Calling await() on losers afterward instead of cancel()
  • Not using a structured scope (coroutineScope) around the race

context