How do `onReceive` and `onSend` work inside `select`, and when would you use each?
answer
- onReceive: ready when channel has an element
- onSend(value): ready when channel can accept
- onReceiveCatching for closed channels (ChannelResult)
- Loser consumes/sends nothing
- onReceive = multiplex inputs; onSend = fan-out outputs
basics
~10 sonReceive waits until a channel has a value to read; onSend waits until a channel can accept a value. Inside select you can wait on several channels and act on whichever is ready first.
solid answer
~40 s`channel.onReceive { value -> ... }` is a `select` clause that becomes ready when the channel has an element; when it wins, it receives that element and runs the lambda with it. `channel.onSend(value) { ... }` becomes ready when the channel can accept `value` (buffer space free or a receiver waiting); on winning it sends `value` and runs the lambda (typically returning `Unit`). You use `onReceive` to **multiplex** several input channels — read from whichever produces first. You use `onSend` to **fan out** to whichever consumer is ready, avoiding blocking on a full channel. Both respect the channel's back-pressure: a clause is simply not ready until the channel allows the operation. For closed channels prefer `onReceiveCatching` (returns a `ChannelResult`) since `onReceive` throws on a closed-for-receive channel.
code
kotlin · 9 linesimport kotlinx.coroutines.channels.*
import kotlinx.coroutines.selects.select
suspend fun balancedSend(x: Int, a: SendChannel<Int>, b: SendChannel<Int>) {
select<Unit> {
a.onSend(x) { /* delivered to a */ }
b.onSend(x) { /* delivered to b */ }
}
}go deeper
Knows onReceive reads and onSend writes, and that select picks whichever channel is ready first.
Explains back-pressure gating, the closed-channel exception vs onReceiveCatching, and that losers do nothing.
Designs mergers/load-balancers with select loops, manages bias-induced starvation, and handles closure cleanly.
Reasons about throughput, fairness, and capacity tuning across many channels, and chooses select vs Flow merge for the workload.
## The two channel clauses A `Channel<E>` is a coroutine-safe queue: producers `send`, consumers `receive`, and it applies **back-pressure** (a full rendezvous/buffered channel suspends senders; an empty channel suspends receivers). `select` lets you wait on channel readiness without committing to a single channel. ### `onReceive` — wait to read ```kotlin select<String> { chanA.onReceive { v -> "A=$v" } chanB.onReceive { v -> "B=$v" } } ``` - **Ready when:** the channel has an element available (or is closed — see below). - **On win:** it actually receives the element and binds it to the lambda parameter. - **On lose:** nothing is consumed from that channel. - **Closed channel:** `onReceive` on a channel closed-for-receive **throws** `ClosedReceiveChannelException`. To handle closure gracefully use `onReceiveCatching { result -> ... }`, where `result` is a `ChannelResult<E>` you inspect with `isClosed`/`getOrNull`. Typical use: **multiplexing / merging** multiple producers — read from whichever channel produces next. ### `onSend` — wait to write ```kotlin select<Unit> { out1.onSend(item) { /* sent to out1 */ } out2.onSend(item) { /* sent to out2 */ } } ``` - **Ready when:** the channel can accept the value (buffer space or a waiting receiver). - **On win:** the value is sent, then the lambda runs (often returning `Unit`). - **On lose:** nothing is sent on that channel. Typical use: **fan-out / load-balancing** — deliver to whichever downstream consumer is free, instead of blocking on one full channel. ## Back-pressure interaction Both clauses are gated by the channel's capacity rules. A clause that cannot proceed (empty for receive, full for send) is just **not ready**; `select` waits. This is how `select` integrates with back-pressure without polling. ## Bias still applies If multiple channels are simultaneously ready, the **first listed** clause wins. Loop with re-evaluated `select` to drain, and consider ordering to avoid starving later channels. ## Worked multiplexer ```kotlin import kotlinx.coroutines.channels.* import kotlinx.coroutines.selects.select suspend fun <T> merge(a: ReceiveChannel<T>, b: ReceiveChannel<T>, out: SendChannel<T>) { while (true) { val item = select<T> { a.onReceive { it } b.onReceive { it } } out.send(item) } } ``` ## When to use which - `onReceive`/`onReceiveCatching`: combining inputs, building mergers, picking the next-available message. - `onSend`: balancing output across consumers, never blocking on a single full sink.
- What happens if you use `onReceive` on a channel that is already closed?It throws `ClosedReceiveChannelException`. Use `onReceiveCatching`, which yields a `ChannelResult` you can test for closure, to avoid the exception.
- Does the value in `onSend(value)` get sent if that clause loses?No — only the winning clause performs its send; losing `onSend` clauses send nothing.
saying these in an interview costs you the question
- Saying onReceive returns silently (instead of throwing) on a closed channel
- Claiming a losing onSend still delivers the value
- Ignoring back-pressure — assuming onSend is always immediately ready
- Confusing onReceive (read) with onSend (write) direction
- Not knowing onReceiveCatching exists for closed-channel handling