skip to content

How do you correctly close a Channel and iterate it, and what is the difference between for-in, consumeEach, and consume?

level: middleimportance: should knowfreq 50%

answer

  1. Producer close(); consumer drains then ends
  2. for-in does NOT cancel on early exit
  3. consumeEach cancels channel on completion/throw
  4. close(cause) re-throws downstream
  5. receiveCatching for exception-free closure

basics

~10 s

The producer calls close() when done. The consumer reads with a for-in loop or consumeEach. consumeEach also cancels the channel when finished, so it cleans up even if something goes wrong.

solid answer

~40 s

The sending side signals completion with close(); buffered values are still delivered, then the receiving side's loop ends. On the consumer side, for (x in channel) repeatedly calls receive until the channel is closed and drained — but it does NOT cancel the channel on an exception. consumeEach { } iterates and, crucially, calls cancel() on the channel when the block completes or throws, ensuring the channel is consumed/cleaned up. consume { } is the general form: it gives you the ReceiveChannel and guarantees cancel() afterward. You can pass an optional cause to close(cause): the consumer then re-throws that cause after draining, which is how you propagate producer failures. Checking isClosedForReceive or using receiveCatching() (returns a ChannelResult) lets you handle closure without exceptions.

go deeper

for a junior

Knows producer closes and consumer iterates with for-in or consumeEach.

for a middle

Distinguishes for-in vs consumeEach cleanup and uses close(cause) to propagate errors.

for a senior

Chooses consume/consumeEach for ownership safety and uses receiveCatching to avoid exception-driven control flow.

for a principal

Designs channel ownership/lifecycle conventions so leaks and double-consumption can't occur across a codebase.

## Closing from the producer The producer owns the lifecycle and calls `close()` when there are no more values. `close()`: - delivers any already-buffered elements first; - then makes the receiving side observe end-of-stream; - makes further `send` throw `ClosedSendChannelException`. You may pass a cause: `close(IllegalStateException("boom"))`. After draining, the consumer re-throws that cause — the standard way to propagate a producer error downstream. ## Three ways to consume ```kotlin import kotlinx.coroutines.channels.* // 1) for-in: simplest; does NOT cancel on early exit/exception for (x in channel) process(x) // 2) consumeEach: iterates, then cancels the channel (even on throw) channel.consumeEach { x -> process(x) } // 3) consume: general scope; cancels channel afterward val first = channel.consume { receive() } ``` - **`for (x in channel)`** — calls `receive` until closed-and-drained. If the loop body throws or you `break`, the channel is **not** cancelled; the producer may leak if no one else consumes. - **`consumeEach { }`** — same iteration, but wrapped so that when the lambda finishes (normally or with an exception) the channel is **cancelled**. Preferred when this coroutine *owns* the channel. - **`consume { }`** — the underlying primitive both above relate to: runs your block with the `ReceiveChannel` as receiver and guarantees `cancel()` in a `finally`. ## Non-exception closure checks To avoid exceptions on a closed channel: - `receiveCatching()` returns a `ChannelResult<T>` you can inspect (`isClosed`, `getOrNull()`). - `isClosedForReceive` / `isClosedForSend` are advisory flags (racy in concurrent use; prefer `receiveCatching`). ## Idempotency `close()` is idempotent: the first call wins and returns true; later calls return false. `cancel()` is harsher — it closes *and* discards buffered elements, and is what `consume`/`consumeEach` use for cleanup. ## Rule of thumb If a coroutine fully owns and reads a channel, use `consumeEach`/`consume` so cleanup is automatic. Use a bare `for` loop only when you deliberately keep the channel alive past the loop.

  • Why might consumeEach be safer than a plain for-loop?
    consumeEach cancels the channel when the block finishes or throws, preventing a leaked/un-drained channel; the for-loop leaves it open on early exit or exception.
  • How do you propagate a producer-side error to the consumer?
    Call close(cause). The consumer delivers buffered items, then re-throws cause from receive/iteration.

saying these in an interview costs you the question

  • Believing for-in cancels the channel on exception
  • Forgetting close() so the consumer hangs
  • Calling send after close and not expecting ClosedSendChannelException
  • Thinking close() discards buffered elements (that's cancel())
  • Using isClosedForReceive as a reliable concurrent guard

context