skip to content

What are trySend, tryReceive, and receiveCatching on a Channel, and when would you use them instead of send/receive?

level: seniorimportance: nice to knowfreq 28%

answer

  1. trySend/tryReceive: non-suspending, return ChannelResult
  2. receiveCatching: suspends but no throw on close
  3. ChannelResult: isSuccess/isFailure/isClosed
  4. Use trySend from non-suspend callbacks
  5. offer/poll are deprecated

basics

~10 s

trySend and tryReceive are non-suspending attempts: they try once and tell you whether it worked, without waiting. receiveCatching waits but returns a result object instead of throwing when the channel is closed.

solid answer

~30 s

send/receive are suspending. trySend(element) and tryReceive() are their non-blocking, non-suspending counterparts: they attempt the operation immediately and return a ChannelResult<T> describing success, failure (channel full/empty), or closed — never suspending and never throwing for those conditions. They're useful in non-suspend contexts (callbacks, hot paths) or with CONFLATED/buffered channels where you want best-effort delivery. receiveCatching() does suspend (waits for a value) but, instead of throwing ClosedReceiveChannelException when the channel closes, returns a ChannelResult you inspect with getOrNull()/isClosed/exceptionOrNull(). This gives exception-free loop termination. The older offer/poll names are deprecated in favour of trySend/tryReceive returning ChannelResult.

go deeper

for a junior

Knows send/receive suspend and that try-variants exist.

for a middle

Uses trySend/receiveCatching and reads ChannelResult states correctly.

for a senior

Chooses non-suspending variants to bridge callbacks and implement drop-based backpressure deliberately.

for a principal

Designs callback/coroutine bridges (callbackFlow, custom adapters) with correct backpressure and closure semantics.

## Suspending vs non-suspending operations - `suspend fun send(e)` / `suspend fun receive()` — wait (suspend) until they can complete; throw on a closed channel. - `fun trySend(e): ChannelResult<Unit>` / `fun tryReceive(): ChannelResult<T>` — **do not suspend**. They attempt once and return immediately. - `suspend fun receiveCatching(): ChannelResult<T>` — **suspends** like `receive`, but reports closure via the result instead of throwing. ## ChannelResult A `ChannelResult<T>` is a small value type encoding one of three outcomes, inspected via: - `isSuccess` / `getOrNull()` — got/sent a value. - `isFailure` — couldn't proceed *right now* (full for send, empty for receive) — only meaningful for the `try*` calls. - `isClosed` / `exceptionOrNull()` — the channel is closed (with optional cause). ```kotlin import kotlinx.coroutines.channels.* val r = channel.trySend(value) when { r.isSuccess -> { /* delivered (or buffered) */ } r.isClosed -> { /* channel closed: r.exceptionOrNull() */ } r.isFailure -> { /* buffer full right now; dropped/retry */ } } ``` ## When to use the non-suspending forms - **Non-suspend contexts**: bridging a callback API or a UI event handler where you cannot call a `suspend` function. `trySend` from inside a `callbackFlow`'s producer is a classic case. - **Best-effort / backpressure-by-dropping**: with a CONFLATED or small buffered channel, `trySend` lets you drop instead of block. - **Hot paths** where suspension overhead is undesirable and missing is acceptable. ## When to use receiveCatching Use it to drain a channel **without** exception-based control flow: ```kotlin while (true) { val res = channel.receiveCatching() val v = res.getOrNull() ?: break // null => closed process(v) } ``` ## Deprecated predecessors `offer()` and `poll()` (which returned a bare value/Boolean) are deprecated; `trySend`/`tryReceive` with `ChannelResult` replaced them because the old API conflated "failed" with "closed."

  • Does trySend ever suspend?
    No. It attempts once and returns a ChannelResult immediately — success, failure (full), or closed. It is callable from non-suspend code.
  • Why prefer receiveCatching over wrapping receive in try/catch?
    It avoids exception-driven control flow for the expected closed case, which is clearer and cheaper than catching ClosedReceiveChannelException.

saying these in an interview costs you the question

  • Saying trySend suspends or waits for space
  • Confusing isFailure (full/empty now) with isClosed
  • Using deprecated offer/poll in new code
  • Thinking receiveCatching is non-suspending
  • Ignoring the dropped value when trySend returns isFailure

context