skip to content

In Dart, which Iterable operations throw on an empty sequence, and what are the safe alternatives such as fold and firstOrNull?

level: middleimportance: should knowfreq 40%

answer

  1. StateError: No element
  2. first, last, single, reduce
  3. single also rejects two or more
  4. fold returns its seed
  5. firstOrNull family since Dart 3.0

basics

~10 s

first, 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 lines
dart
void 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

for a junior

Know that first and reduce throw on an empty Iterable, and that firstOrNull and fold do not.

for a middle

Explain reduce versus fold, the two failure modes of single, and why firstWhere with a null orElse fails under null safety.

for a senior

Choose total or throwing operations deliberately, treating a StateError as an assertion only where empty input really is a bug.

for a principal

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.