skip to content

Explain awaitItem(), awaitComplete(), and awaitError() in Turbine. What does each assert and return?

level: middleimportance: must knowfreq 65%

answer

  1. Item events end in one terminal: Complete or Error
  2. awaitItem returns T; awaitError returns Throwable
  3. awaitComplete returns Unit, asserts normal end
  4. Wrong event type -> AssertionError
  5. Strict ordering; one terminal only

basics

~10 s

awaitItem() waits for the next value and returns it. awaitComplete() checks the flow finished normally. awaitError() checks the flow stopped because of an exception and returns that exception.

solid answer

~40 s

Inside flow.test { }, these three suspending functions consume Turbine's event queue and assert the event type. awaitItem(): T suspends until the next emission and returns the value; it fails if the next event is a completion or error instead. awaitComplete() asserts the next event is normal completion and returns Unit; it fails if an item or error arrives. awaitError(): Throwable asserts the next event is a terminal failure and returns the thrown exception so you can assert its type/message. All three respect a timeout (default 3s wall-clock, or virtual time under runTest) and throw an AssertionError if the wrong event type appears or nothing arrives. Because they consume in order, the sequence of calls must match the flow's actual emission-then-terminal sequence exactly, or Turbine reports an unexpected event.

code

kotlin · 12 lines
kotlin
@Test
fun errorPath() = runTest {
    flow {
        emit(1)
        throw IllegalStateException("boom")
    }.test {
        assertEquals(1, awaitItem())
        val t = awaitError()
        assertIs<IllegalStateException>(t)
        assertEquals("boom", t.message)
    }
}

go deeper

for a junior

Knows the three functions roughly map to value, normal end, and error.

for a middle

States return types, that exactly one terminal occurs, and that wrong event types fail with AssertionError.

for a senior

Explains strict ordering, timeout behavior under runTest virtual time, and capturing the throwable from awaitError for assertions.

for a principal

Discusses how the event-queue model makes flow tests deterministic and how to design assertions that document the full emission contract.

## The event model Turbine models a flow as an ordered queue of **events**: zero or more **Item** events, ended by exactly one **terminal** event — either **Complete** (the flow finished normally) or **Error** (it threw). The await functions pop the front of this queue and assert its type. ## `awaitItem(): T` Suspends until the next event is available, then: - If it's an **Item**, returns the value `T`. - If it's **Complete** or **Error**, throws an `AssertionError` (you expected a value, got a terminal). - If nothing arrives within the timeout, throws a timeout `AssertionError`. ```kotlin val value: Int = awaitItem() assertEquals(42, value) ``` ## `awaitComplete()` Asserts the next event is **normal completion**. Returns `Unit`. Fails if the next event is an item or an error. Use it once, after consuming all expected items, to prove the flow ended cleanly. ## `awaitError(): Throwable` Asserts the flow terminated by **throwing**, and **returns the throwable** so you can inspect it: ```kotlin flow<Int> { throw IllegalStateException("boom") }.test { val t = awaitError() assertIs<IllegalStateException>(t) assertEquals("boom", t.message) } ``` Fails if the flow completes normally or emits another item first. ## Ordering is strict The await calls must mirror the actual sequence. For a flow that emits `1`, `2`, then completes you must call `awaitItem()` twice and `awaitComplete()` once. Calling `awaitComplete()` while an unconsumed item remains is a failure — order is part of the assertion. ## Timeouts Each await respects Turbine's timeout. Under plain coroutines it's a real **3-second** default (configurable via the `timeout` parameter on `test`). Under `runTest`, virtual time means a `delay()` in the flow is skipped, but a flow that simply never emits will still trip the timeout. ## Common combinations - Finite happy path: N × `awaitItem()` then `awaitComplete()`. - Failure path: maybe some `awaitItem()`, then `awaitError()`. - Never assert both `awaitComplete()` and `awaitError()` for the same flow — there is exactly one terminal.

  • What happens if you call awaitComplete() but the flow actually emits another item?
    Turbine throws an AssertionError reporting it expected completion but received an item — the event-type mismatch fails the test.
  • How do you assert on the thrown exception's type and message?
    Capture the return of awaitError() into a val and assert on it, e.g. assertIs<MyException>(t) and assertEquals("...", t.message).

saying these in an interview costs you the question

  • Thinking awaitComplete() returns the last item
  • Believing awaitError() only checks a flag and can't return the exception
  • Calling both awaitComplete and awaitError for one flow
  • Ignoring that ordering of await calls is enforced
  • Assuming await functions never time out

context