skip to content

How do you assert that a flow emits some values and then throws, or that it emits nothing at all, when collecting with toList()?

level: seniorimportance: should knowfreq 47%

answer

  1. toList() rethrows on flow failure -> assertFailsWith
  2. Need prefix + error: collect{} into external list + runCatching
  3. Empty flow toList() -> emptyList()
  4. first() throws on empty; firstOrNull() -> null
  5. catch{} operator handles errors inside the chain

basics

~20 s

If a flow throws, toList() rethrows that exception, so wrap it in assertFailsWith. To capture values emitted before the failure, collect into a list manually with collect{}. For an empty flow, toList() returns an empty list you assert with assertEquals(emptyList(), result).

solid answer

~40 s

A flow that completes with an exception causes toList() to throw at the point of failure, discarding the partial list. To assert the exception type use assertFailsWith<SomeException> { flow.toList() } (kotlin.test). To assert BOTH the values emitted before the error AND the error, accumulate into an external MutableList inside collect { received += it } wrapped in a try/catch or runCatching, then assert the captured prefix and the exception separately. For an empty flow, toList() yields an empty list: assertEquals(emptyList<T>(), result) or assertTrue(result.isEmpty()). Use first() carefully—it throws NoSuchElementException on empty, while firstOrNull() returns null. count() asserts size only. These patterns avoid Turbine; they rely on standard kotlinx-coroutines terminal operators plus kotlin.test assertions inside runTest.

code

kotlin · 7 lines
kotlin
@Test
fun catchOperatorRecovers() = runTest {
    val result = flow { emit(1); throw RuntimeException() }
        .catch { emit(-1) }        // turn error into a fallback emission
        .toList()
    assertEquals(listOf(1, -1), result)  // flow now completes normally
}

go deeper

for a junior

Knows toList() throws when the flow throws and an empty flow gives an empty list.

for a middle

Uses assertFailsWith and firstOrNull, and knows the empty-flow behaviors.

for a senior

Captures prefix-plus-error via manual collect/runCatching and explains the catch operator and exception transparency.

for a principal

Codifies error-path testing patterns and guards against exception-transparency violations in shared flow utilities.

## Three scenarios ### 1. Flow throws (exceptional completion) When a flow's upstream throws, the exception propagates **through the terminal operator**. `toList()` will throw that exception, and any values emitted before the throw are **lost** from `toList()`'s return value (it never returns). To assert the exception use `assertFailsWith`: ```kotlin @Test fun flowThrows() = runTest { val flow = flow { emit(1) throw IllegalStateException("boom") } val ex = assertFailsWith<IllegalStateException> { flow.toList() } assertEquals("boom", ex.message) } ``` ### 2. Capture emissions AND the error If you need the values emitted **before** the failure, don't rely on `toList()`—collect manually into an external list and catch: ```kotlin @Test fun valuesThenError() = runTest { val received = mutableListOf<Int>() val error = runCatching { flow { emit(1); emit(2); throw RuntimeException("x") } .collect { received += it } }.exceptionOrNull() assertEquals(listOf(1, 2), received) assertTrue(error is RuntimeException) } ``` Note: never wrap the `emit` in a try/catch *inside* the flow to swallow downstream exceptions—that violates flow **exception transparency**. Catching in the *collector* (outside the builder) is fine; inside the builder use the `catch { }` operator instead. ### 3. Empty flow A flow that completes with no emissions: `toList()` returns an empty list. ```kotlin @Test fun emptyFlow() = runTest { val result = emptyFlow<Int>().toList() assertEquals(emptyList(), result) // or assertTrue(result.isEmpty()) assertEquals(0, emptyFlow<Int>().count()) // count() = size assertion assertNull(emptyFlow<Int>().firstOrNull()) // first() would THROW here } ``` ## Operators recap - `toList()` — full list, **rethrows** on failure. - `collect { }` — manual; lets you accumulate a prefix and catch externally. - `catch { }` — intermediate operator to handle upstream exceptions inside the chain (e.g. emit a fallback) so the flow completes normally. - `count()` — number of emissions. - `first()` / `firstOrNull()` — first or first-or-null (throws vs null on empty). ## Assertions recap `assertFailsWith<T> { }`, `assertEquals(emptyList(), result)`, `assertTrue(result.isEmpty())`, `assertNull(...)` — all from `kotlin.test`, inside `runTest`. ## Keywords exception transparency, `assertFailsWith`, `runCatching`, `catch` operator, `emptyFlow`, `count()`, `firstOrNull()`.

  • Why can't you just read toList()'s return value to see what was emitted before an exception?
    Because toList() never returns when the flow throws—the exception propagates instead, so there is no partial list. You must collect manually into an external list.
  • What is exception transparency and why does it matter here?
    A flow must not catch exceptions thrown downstream of its emit; you handle upstream errors with the catch operator. Wrapping emit in try/catch breaks this invariant and can hide cancellation.

saying these in an interview costs you the question

  • Expecting toList() to return the values emitted before a throw
  • Wrapping emit in try/catch inside the flow builder
  • Using first() on a possibly-empty flow without expecting NoSuchElementException
  • Confusing the catch operator with a collector-side try/catch
  • Asserting size with assertEquals on the list itself when count() is clearer

context