skip to content

What is the role of awaitClose { } in callbackFlow, and what happens if you omit it?

level: middleimportance: must knowfreq 65%

answer

  1. awaitClose suspends until close/cancel, then cleans up
  2. must be the last statement
  3. omitting it throws IllegalStateException at runtime
  4. fires on downstream cancellation too
  5. prevents listener leaks

basics

~10 s

awaitClose keeps the flow alive until it is cancelled or closed, then runs cleanup like unregistering the listener. If you leave it out, the flow ends immediately and Kotlin throws an error.

solid answer

~40 s

awaitClose { } suspends the callbackFlow producer block until the channel is closed or the flow's collection is cancelled. Its lambda is the teardown hook: you unregister the listener / free the resource there. It must be the last call because the block would otherwise return as soon as it finishes registering, which would close the producer channel and complete the flow prematurely while the callback is still wired up. If you omit awaitClose entirely, kotlinx.coroutines detects it and throws an IllegalStateException at runtime ('awaitClose { } should be used in the end of callbackFlow block') to prevent silent listener leaks. awaitClose resumes on either normal close()/cancel from inside, or on downstream cancellation, guaranteeing cleanup runs exactly once in both paths.

go deeper

for a junior

Knows awaitClose is where you unregister the listener and that it is required.

for a middle

Explains why the block can't return early and that omission throws IllegalStateException.

for a senior

Articulates the exactly-once cleanup across close and cancellation paths and keeps teardown fast/non-suspending.

for a principal

Treats awaitClose as a lifecycle contract; audits adapters for leak-safe teardown and consistent patterns.

## Why the block can't just return The `callbackFlow { }` lambda runs once per collection. After you register your listener, there is **no more sequential work to do** — the values arrive later via the callback. If the lambda simply returned, the underlying channel (the `ProducerScope`) would be closed, the flow would complete, and your listener would still be registered against the source: a **resource/listener leak** and dead emissions. `awaitClose { }` fixes this by **suspending** the producer coroutine. It does not busy-wait; it parks the coroutine until one of these happens: - The producer calls `close()` or `cancel()` from inside the block. - The **collector is cancelled** (its scope dies, a `take(n)` completes, a timeout fires, etc.), which cancels the producer. When it resumes, it runs the supplied lambda — your **cleanup** — and then the flow finishes. ```kotlin fun sensorFlow(sensor: Sensor): Flow<Reading> = callbackFlow { val listener = Sensor.Listener { r -> trySend(r) } sensor.register(listener) awaitClose { // runs on close OR downstream cancellation sensor.unregister(listener) } } ``` ## The omission trap If you forget `awaitClose`, kotlinx.coroutines throws: ``` IllegalStateException: 'awaitClose { }' should be used in the end of the callbackFlow block. Otherwise, a callback/listener may leak in case of external cancellation. ``` This is intentional fail-fast behavior — the library cannot know how to unregister your listener, so it forces you to provide the hook. ## Guarantees - **Exactly-once cleanup** across both the normal-close path and the cancellation path. - awaitClose's lambda runs in the producer's context; keep it **non-suspending and quick** (it's teardown). ## Key APIs `awaitClose`, `ProducerScope`, `close`, `cancel`, `trySend`, `IllegalStateException`.

  • Does awaitClose's lambda run if the collector is cancelled from outside?
    Yes. Downstream cancellation propagates to the producer, resumes awaitClose, and runs the cleanup lambda.
  • Can you do suspending work inside awaitClose?
    It accepts a plain (non-suspending) lambda meant for fast teardown; don't perform long suspending operations there.

saying these in an interview costs you the question

  • Thinks awaitClose is optional
  • Puts awaitClose before listener registration or in the middle
  • Believes cleanup only runs on close() but not on cancellation
  • Says awaitClose blocks a thread
  • Claims omission silently leaks with no error

context