Inside a callbackFlow, when do you use trySend versus send, and how do you handle backpressure?
answer
- trySend = non-suspending, returns ChannelResult
- send = suspending, needs a coroutine context
- default channel is rendezvous -> trySend can fail
- shape overflow with buffer(capacity, onBufferOverflow)
- DROP_OLDEST/DROP_LATEST/SUSPEND for policy
basics
~10 sUse trySend in a normal (non-suspending) callback because it never blocks; it just succeeds, fails, or signals closed. Use send only from suspending code that can wait. Control overflow with a buffer.
solid answer
~40 strySend(value) is non-suspending: it attempts to offer into the channel and returns a ChannelResult that is success, failure (buffer full / no capacity), or closed. It's the correct choice inside ordinary listener callbacks whose signatures can't suspend. send(value) suspends until there is capacity, so it can only be called from a suspending context (e.g., a coroutine you launch inside the block). The default callbackFlow channel is rendezvous-like (the buffered RENDEZVOUS), so a fast producer can have trySend fail. You shape backpressure with the buffer() operator or by passing onBufferOverflow (BufferOverflow.SUSPEND, DROP_OLDEST, DROP_LATEST) — typically callbackFlow(...).buffer(capacity, onBufferOverflow). Dropping with trySend silently discards events, which may or may not be acceptable; choose deliberately and consider checking the ChannelResult.
go deeper
Knows trySend is used in callbacks and doesn't suspend.
Distinguishes trySend (non-suspending, ChannelResult) from send (suspending, needs coroutine).
Reasons about the default rendezvous channel, buffer()/BufferOverflow policies, and inspecting ChannelResult.
Designs an explicit backpressure/overflow contract for adapters and weighs event-loss vs latency trade-offs system-wide.
## trySend vs send Inside `callbackFlow` you are in a `ProducerScope<T>`, which exposes both: - **`trySend(value): ChannelResult<Unit>`** — *non-suspending*. Immediately tries to put the value in the channel. Returns: - **success** when buffered/received, - **failed** when there's no capacity (buffer full / rendezvous with no waiting receiver), - **closed** when the channel is already closed. Because callbacks usually have a plain (non-`suspend`) signature, `trySend` is the natural fit. - **`send(value)`** — *suspending*. Waits until there is capacity. You can only call it from a coroutine, e.g. one you `launch { }` inside the block. Use it when you genuinely want backpressure to slow the producer and the producing path can suspend. ```kotlin callbackFlow { val cb = Api.Callback { v -> val result = trySend(v) // never suspends if (result.isFailure && !result.isClosed) { // buffer full: log/metric/drop decision } } api.register(cb) awaitClose { api.unregister(cb) } } ``` ## Backpressure & buffering The channel under `callbackFlow` defaults to a **rendezvous-style** buffer, so a fast async source can overrun a slow collector and `trySend` will fail. Reshape it with the **`buffer`** operator: ```kotlin callbackFlow { /* ... */ } .buffer(capacity = 64, onBufferOverflow = BufferOverflow.DROP_OLDEST) ``` - **`BufferOverflow.SUSPEND`** (default with a fixed capacity) — back-pressures the sender; with `trySend` this means failure once full, since trySend won't suspend. - **`BufferOverflow.DROP_OLDEST` / `DROP_LATEST`** — keep flowing by discarding, good for state-like streams where only the latest matters. - **`Channel.CONFLATED`** / `conflate()` — keep only the most recent value. ## Decision guide - Synchronous callback that cannot suspend -> **trySend** (+ a buffering/overflow policy). - You can suspend and want strict backpressure -> **send** from a launched coroutine. - Don't ignore the `ChannelResult` from `trySend` if dropping events is unacceptable. ## Key APIs `trySend`, `send`, `ChannelResult` (`isSuccess`/`isFailure`/`isClosed`), `buffer`, `BufferOverflow`, `conflate`, `ProducerScope`.
- How would you call send from inside a synchronous callback?You can't directly; wrap it in launch { send(v) } using the ProducerScope's coroutine context, accepting the extra coroutine and ordering caveats — or just use trySend with a buffer.
- What does a failed trySend mean and how do you react?It means no capacity (or closed). Inspect ChannelResult: if isClosed handle termination; otherwise the value was not delivered, so log/drop/conflate per your policy.
saying these in an interview costs you the question
- Thinks trySend suspends or blocks the callback thread
- Calls send directly in a non-suspending callback
- Ignores trySend's ChannelResult and assumes delivery
- Unaware the default channel can drop/fail under load
- Confuses buffer() overflow policies