How do you terminate a callbackFlow from inside, and how does close() differ from cancel()?
answer
- close() = normal end; close(error) = error end
- cancel() = cancellation semantics
- both resume awaitClose for cleanup
- close on source onClosed, close(e) on onFailure
- external cancellation = collector stops, not you calling close
basics
~10 sCall close() when the source is finished normally; collectors stop and cleanup runs. Call cancel() to stop with an error/cancellation. Both resume awaitClose so your listener gets unregistered.
solid answer
~40 sFrom inside the callbackFlow block (a ProducerScope) you can finish the stream yourself. close(cause: Throwable? = null) completes the underlying channel: with no cause the flow ends normally (collector's collect returns); with a cause the flow completes exceptionally and the collector sees that exception. cancel(cause: CancellationException? = null) cancels the producer scope, terminating with cancellation semantics. In both cases awaitClose resumes so your teardown runs. You typically call close() from the callback when the source signals end-of-stream (e.g., a websocket onClosed), or close(error) on a fatal source error (e.g., onFailure). Don't confuse internal close() with the external/downstream cancellation that occurs when the collector's scope dies — that also resumes awaitClose but is initiated outside the block.
go deeper
Knows close() ends the flow and that there is a way to end it with an error.
Differentiates close() (normal), close(error), and cancel(); maps them to source callbacks.
Explains internal vs external/downstream termination and that awaitClose cleanup runs in all paths.
Defines termination/error-propagation conventions for adapters so collectors get consistent completion/error semantics.
## Terminating from inside the block `callbackFlow { }` gives you a `ProducerScope<T>`. You complete the flow by closing the producer channel: - **`close(cause: Throwable? = null): Boolean`** - `close()` (no cause) -> the flow **completes normally**; the collector's `collect { }` returns cleanly. - `close(error)` -> the flow **completes exceptionally**; the collector observes `error` (rethrown from `collect`). - **`cancel(cause: CancellationException? = null)`** -> cancels the producer scope with **cancellation** semantics (not a normal completion). Both paths cause `awaitClose` to resume and run your cleanup lambda. ```kotlin fun socketMessages(socket: Socket): Flow<Message> = callbackFlow { val listener = object : Socket.Listener { override fun onMessage(m: Message) { trySend(m) } override fun onClosed() { close() } // normal end override fun onError(e: Throwable) { close(e) } // error end } socket.connect(listener) awaitClose { socket.disconnect() } } ``` ## close vs cancel | | Completion seen by collector | Typical use | |---|---|---| | `close()` | normal completion | source signaled done | | `close(error)` | rethrows `error` | source reported a failure | | `cancel(cause)` | cancellation | abort the stream as cancelled | ## Internal close vs external cancellation - **Internal** `close()`/`cancel()` are *you* ending the stream from the producer. - **External / downstream** cancellation happens when the **collector** stops (scope cancelled, `take(n)` reached, timeout). That also resumes `awaitClose`, but is driven from outside the block — you don't call anything. Either way the contract holds: **awaitClose runs cleanup exactly once.** ## Key APIs `ProducerScope.close`, `cancel`, `awaitClose`, `SendChannel.close`, `CancellationException`.
- If you call close(IOException()), what does the collector experience?The collect call rethrows that IOException, so the collector's surrounding try/catch (or a catch operator) can handle it.
- Does close() unregister your listener?Not directly. close() completes the flow which resumes awaitClose, and your awaitClose lambda is what unregisters the listener.
saying these in an interview costs you the question
- Thinks close() throws to the collector by default
- Believes cancel() and close() are identical
- Says close() unregisters the listener automatically without awaitClose
- Confuses internal close with downstream cancellation
- Claims you must cancel the whole scope to end the flow