In Dart, which Iterable operations throw on an empty sequence, and what are the safe alternatives such as fold and firstOrNull?
answer
- StateError: No element
- first, last, single, reduce
- single also rejects two or more
- fold returns its seed
- firstOrNull family since Dart 3.0
basics
~10 sfirst, last, single, reduce and firstWhere without orElse throw a StateError on an empty Dart Iterable. fold returns its initial value, and firstOrNull, lastOrNull, singleOrNull and elementAtOrNull return null instead.
solid answer
~40 s`first`, `last`, `single` and `reduce` assume at least one element and throw `StateError('No element')` when there is none; `single` also throws `StateError('Too many elements')` for two or more, and `firstWhere`/`lastWhere` throw when nothing matches and no `orElse` is given. Safe, total alternatives: `fold(initial, combine)` returns `initial` for an empty input and may produce a different type than the elements, which `reduce` cannot; and since Dart 3.0 `firstOrNull`, `lastOrNull`, `singleOrNull` and `elementAtOrNull` return `null` instead of throwing, exported from `dart:core` without an import. For a find-or-null on a non-nullable type use `where(test).firstOrNull`, because `firstWhere(..., orElse: () => null)` does not compile. Keep the throwing forms where an empty input is a bug you want to surface.
code
dart · 15 linesvoid main() {
final logLines = ['INFO boot', 'INFO ready'];
final errors = logLines.where((l) => l.startsWith('ERROR'));
print(errors.firstOrNull); // null
print(errors.fold<int>(0, (n, l) => n + l.length)); // 0
try {
errors.reduce((a, b) => '$a\n$b');
} on StateError catch (e) {
print(e.message); // No element
}
print(['x', 'y'].singleOrNull); // null: more than one element
}go deeper
Know that first and reduce throw on an empty Iterable, and that firstOrNull and fold do not.
Explain reduce versus fold, the two failure modes of single, and why firstWhere with a null orElse fails under null safety.
Choose total or throwing operations deliberately, treating a StateError as an assertion only where empty input really is a bug.
Encourage codebase conventions that make emptiness explicit in pipelines, so crashes on a clean log or empty filter are designed out.
## The operations that throw on an empty Iterable Several `Iterable` members assume there is at least one element and throw a **`StateError`** when there is none: | Member | Empty iterable | More than one element | |---|---|---| | `first`, `last` | `StateError('No element')` | fine | | `single` | `StateError('No element')` | `StateError('Too many elements')` | | `reduce(combine)` | `StateError('No element')` | fine | | `firstWhere(test)` / `lastWhere(test)` with no `orElse` and no match | `StateError` | fine | | `elementAt(i)` beyond the end | `RangeError` | fine | A pipeline over log lines hits these easily: `logLines.where(isError).first` throws on a clean log, and `durations.reduce((a, b) => a + b)` throws when a filter removed everything. ## Safer alternatives - **`fold(initialValue, combine)`** starts from `initialValue`, so an empty iterable simply returns it: `durations.fold(0, (sum, d) => sum + d)` is `0` for no lines. - **`firstOrNull`, `lastOrNull`, `singleOrNull`, `elementAtOrNull(i)`** (Dart 3.0) return `null` instead of throwing. They are extension members defined in `dart:collection` and exported from `dart:core`, so they work without an import. `singleOrNull` returns `null` both for zero and for two or more elements. - **`where(test).firstOrNull`** is the idiomatic "find or null" when the element type is non-nullable. - **`isEmpty` / `isNotEmpty` guards** are fine for a `List`, but on a lazy iterable they start an extra pass. ## reduce versus fold | | `reduce` | `fold` | |---|---|---| | Starting value | the first element | an explicit `initialValue` | | Empty input | throws `StateError` | returns `initialValue` | | Result type | the element type `E` | any type `T` | | Typical use | max or sum of a list known to be non-empty | totals, counts, building a different type | Because `reduce`'s result must be the element type, summing string lengths needs `fold`: `lines.fold<int>(0, (total, line) => total + line.length)`. `reduce` cannot do it without first mapping to lengths, and would still throw on no lines. ## The firstWhere orElse trap With sound null safety, `firstWhere` on an `Iterable<String>` must return a `String`, and `orElse` must return one too. So this does not compile: ```dart final match = logLines.firstWhere( (l) => l.startsWith('FATAL'), orElse: () => null, // error: null isn't a String ); ``` The fixes are `logLines.where((l) => l.startsWith('FATAL')).firstOrNull`, which yields `String?`, or an `orElse` that returns a real sentinel value when one makes sense. ## Filtering by type and position Two more helpers that make pipelines total rather than throwing or casting: - **`whereType<T>()`** keeps elements of type `T` and returns `Iterable<T>`, replacing the verbose `where((e) => e is T).cast<T>()`; Effective Dart recommends it. - **`indexed`** pairs each element with its position as a record `(int, E)`, so `for (final (i, line) in logLines.indexed)` avoids a separate counter or an `elementAt` loop. ## Choosing in an interview answer 1. If an empty result is **normal** (a clean log, a filter that matches nothing), use `fold`, `firstOrNull` or another total operation. 2. If an empty result is a **bug**, `first`, `single` or `reduce` throwing a `StateError` is a reasonable assertion — but then say so, and consider a clearer error message at the call site. 3. For `single`, remember the second failure mode: more than one element also throws. The underlying rule: know which operations are **total** (defined for every input) and which are **partial** (throw on some inputs), and choose the partial ones only when you can prove, or want to assert, that the input is non-empty.
- Why does logLines.firstWhere(test, orElse: () => null) fail to compile for a Dart List<String>?`firstWhere` returns the element type, `String`, and `orElse` must return a `String` too; `null` is not a `String` under sound null safety. Use `logLines.where(test).firstOrNull`, which returns `String?`, or give `orElse` a real sentinel string.
- When would you keep reduce rather than switch to fold in Dart?When the input is guaranteed non-empty and the result has the element type, such as the maximum of a validated list; `reduce` then reads naturally and a `StateError` on empty input acts as an assertion. For totals of a different type, or input that may be empty, use `fold`.
- What does singleOrNull return for a Dart iterable with three elements?`null`. It returns the element only when there is exactly one; zero and two or more both give `null`. That differs from `single`, which throws `StateError('No element')` or `StateError('Too many elements')` for those cases.
saying these in an interview costs you the question
- first returns null on an empty Iterable.
- reduce returns zero for an empty list.
- single only fails when the Iterable is empty.
- firstOrNull needs an import from package:collection.
- fold and reduce behave identically on empty input.