When is a NoSuchElementException thrown for a call to flow.single()

Viewed 2001

Say I have an API like so:

interface Foo {
   val barFlow: Flow<Bar>
}

And I consume it like so:

class FooConsumer(private val foo: Foo) {
   init {
       CoroutineScope(Dispatchers.IO).launch {
           val bar = foo.barFlow.single()
           println("Collected bar: $bar)
       }
   }
}

According to the docs for single a NoSuchElementException can be thrown if the flow is empty. However, this confuses me quite a lot, as a terminal operation on a flow will "await" elements of the flow to be emitted. So how will the call to single know that there were no elements in the flow? Maybe an element just hasn't been emitted yet?

I mean under the hood, the call to single is collecting the source flow before it does the check. Therefore at least 1 item must have been emitted before the check for null is carried out, so that null check should never succeed and a NoSuchElementException should never be thrown (for the case where the flow is of a non nullable type).

So will NoSuchElementException only be a possibility for flows of nullable types?

Here is the source code for single:

/**
 * The terminal operator, that awaits for one and only one value to be published.
 * Throws [NoSuchElementException] for empty flow and [IllegalStateException] for flow
 * that contains more than one element.
 */
public suspend fun <T> Flow<T>.single(): T {
    var result: Any? = NULL
    collect { value ->
        if (result !== NULL) error("Expected only one element")
        result = value
    }

    if (result === NULL) throw NoSuchElementException("Expected at least one element")
    @Suppress("UNCHECKED_CAST")
    return result as T
}
2 Answers

NoSuchElementException is thrown when the Flow finishes its emission without emitting a single element. One case I can think of right now is when you need to turn a collection into a Flow source. If that collection is empty and you call single on that Flow you will get a NoSuchElementException.

This example may seem absurd but you get the point:

val emptyListFlow = emptyList<Int>().asFlow()

launch {
    val data = emptyListFlow.single()
}

In my case, I made a list.first(), where the list was empty

Related