Bad State: No Element — Guard firstWhere
Pass orElse to firstWhere: Bad state No element means a search found nothing.
20+ years shipping production backend systems. Written from production experience, not tutorials.
- ✓Dart collections basics: lists and iterables
- ✓A Flutter screen that searches a list
- ✓flutter test running in your project
- Bad state: No element means firstWhere, singleWhere, first, or last found nothing to return — usually an empty list or a filter that matches zero items
- Pass orElse to firstWhere so misses return a fallback instead of throwing, and check isEmpty before touching first or last
- Use singleWhere only when the data guarantees exactly one match; duplicates throw a different Bad state error about too many elements
- Validate with empty, miss, and duplicate test cases so search failures return fallbacks in CI instead of red screens for users
Imagine asking a librarian for the first book by an author the library does not carry. A good librarian says sorry, we do not have it — that is orElse. Dart's default librarian instead shouts and knocks over the desk — that is Bad state: No element. The shelf was empty or no spine matched, and rather than admitting it gracefully, the search threw a tantrum. Handing every search a fallback instruction turns the tantrum into a polite apology your UI can actually display.
Your list screen works for months, then one user with an empty inbox opens it and meets a red screen: Bad state: No element. No network failure, no corrupt data — just a search across zero items. FirstWhere, singleWhere, first, and last all throw this identical tantrum when there is nothing to return, and the stack points at a line that looked perfectly safe during development.
The trap is sample data. Developers build against lists that always contain the sought item, so the miss path never executes until production supplies an empty cart, a filtered-to-zero search, or a deleted record the cache still references. The throw happens synchronously inside build, which means no error boundary of async handling catches it — the frame dies and the red screen takes over.
This guide hardens every lookup. You will learn which methods throw and why, how orElse converts misses into fallbacks, when singleWhere's duplicate error bites, which empty-list guards belong before every read, and how nullable search patterns keep intent clear. By the end, empty data will render empty states instead of exceptions.
Which Calls Throw and Exactly Why
Four call shapes share one tantrum. FirstWhere throws when no element satisfies the predicate — the classic filter miss. First and last throw on empty collections, since there is no position zero or final position to return. SingleWhere throws on zero matches like firstWhere, and on two or more matches, because its contract promises exactly one. Indexing an empty list throws RangeError instead, a close cousin worth guarding in the same pass.
All four throw synchronously at the call site. Inside build that means the frame aborts to the red error screen; inside event handlers it unwinds to the nearest catch or crashes the zone. There is no async boundary to soften it and no null to check downstream — the method never returns on the miss path. Developers who expect a null result write code after the call that never executes, compounding the confusion.
Learn to spot the shapes in review. A firstWhere without a trailing orElse argument is an unhandled miss path. A .first or .last with no isEmpty guard above it is an unhandled empty path. A singleWhere on data without a uniqueness proof is an unhandled duplicate path. Each shape takes seconds to recognize and minutes to harden — the cheapest review comments you will ever leave.
orElse: Turning Misses Into Fallbacks
The orElse callback is the designed answer to the miss path: return a fallback element when nothing matches, and the method never throws. A checkout resolving a possibly deleted address supplies a fallback that routes to the picker; a theme lookup supplies the default theme; a settings read supplies factory defaults. The UI already knows how to render these states — orElse simply lets the miss reach them instead of dying mid-build.
Choose fallbacks that preserve user momentum. A deleted-address fallback that opens the address picker with a notice keeps checkout moving; a fallback that silently substitutes another user's address corrupts orders. The fallback runs only on misses, so make it cheap and side-effect free — no network calls inside orElse, just a value selection. Expensive recovery belongs in the UI branch the fallback triggers, not in the callback.
Apply orElse universally, not just at the crashed site. After any No element incident, search the codebase for every firstWhere lacking the argument — each is the same bug with a different predicate. Fixing one while leaving twenty is incident scheduling, not incident response. The review rule is absolute: no firstWhere merges without orElse, no exceptions, no todos. Automate the check and the rule enforces itself.
singleWhere Duplicates and first-last Guards
SingleWhere promises exactly one match, so it throws twice: zero matches produce No element, two or more produce Too many elements. The duplicate error surprises teams that proved uniqueness once and watched imports, merges, or race conditions quietly duplicate rows. If your data cannot prove uniqueness at the call site — a database constraint, a Set-backed structure — singleWhere is a promise you cannot keep.
Downgrade deliberately when any-match suffices. FirstWhere with orElse expresses honestly that duplicates are tolerable and misses have fallbacks, surviving both error shapes. Keep singleWhere only where duplicates indicate corruption worth crashing over in debug and catching in production — and even then, wrap the call so the corruption renders diagnostics instead of red.
First and last need the humble isEmpty guard. Check emptiness before reading and return the designed empty state: the illustration, the invite button, the zero balance. Guards belong directly above the read, not three frames up the call stack, so future readers see the contract locally. Index access shares the rule — verify indices against length before subscripting lists that async gaps can shrink. Co-locating guard and read keeps the contract visible through every future refactor.
Nullable Search Patterns for Honest Misses
Some searches legitimately find nothing, and the code should say so in its types. A manual loop returning a nullable result expresses the miss honestly: found means an element, null means absence, and every caller handles both because the compiler insists. This pattern suits heterogeneous fallback logic where orElse's single value cannot capture what the UI must do next.
IndexWhere plus an index check is the list-position sibling: search for the position, compare against -1, and branch explicitly. It shines when callers need the position itself — highlighting the found row, scrolling to it, or replacing it. The -1 branch is the designed miss path, as visible as orElse and equally unthrowing.
Keep the patterns boring and local. A tiny private helper per screen beats a clever generic extension that future readers must decode, and explicit branches beat sentinel values that conflate missing with real data. Misses are normal application states — empty inboxes, cleared carts, deleted records — so their code should read as calmly as the empty-state illustration looks. Boring miss handling is the hallmark of production-ready lookup code. Reviewers should praise calm fallbacks, not clever queries.
Stale Caches: Validating Ids Before Lookup
The nastiest misses come from ids that were valid yesterday. Mobile caches outlive the records they reference: addresses deleted on the web, cards removed by support, products delisted overnight. The lookup code is correct against fresh data and explosive against reality — the gap between cache time and lookup time is where No element breeds.
Validate at the boundary, not at the crash. When checkout opens, reconcile cached ids against the fresh fetch: drop dangling references, clear them from storage, and fall through to the picker before any firstWhere runs. This converts a crash into a minor inconvenience — reselect an address — at the moment the user can best absorb it, rather than mid-payment where trust is thinnest.
Design caches with deletion in mind from the start. Store the fetched-at timestamp alongside ids, revalidate selections older than the session on entry to critical flows, and listen for deletion events that proactively purge references. Caches are performance tools, not source-of-truth — every cached id is a hypothesis the next fetch may falsify, and lookup code must survive falsification gracefully. Purge-on-delete beats crash-on-read in every postmortem. Design for deletion from day one.
Locking It With Empty, Miss, and Duplicate Tests
Three fixtures harden every lookup: the empty collection, the populated collection with no match, and the collection with duplicates. Pump the widget once per fixture and assert the designed outcome — empty illustration, fallback content, first-match rendering — plus tester.takeException is null throughout. This trio covers both throw shapes and the guard paths in under twenty lines per call site.
Name tests after the miss, not the method. Test titles like dangling address id routes to picker document the contract for future readers, while firstWhere test says nothing about intent. When a new developer changes the lookup, the failing test title explains the requirement instead of merely announcing a breakage.
Run the trio in CI for every screen that searches. Lookup regressions arrive disguised as data changes — a backend that starts returning empty arrays, an import that duplicates rows — and only fixture tests catch them before users do. Screens covered by the trio render empty states with the same polish as their populated states, because both are tested with equal seriousness. Empty is a first-class state, so test it like one. Fixtures this small pay for themselves on the first data change.
Deleted Address Crashed Checkout for 4,100 Users
- Never assume a cached id still exists. Any lookup against data another surface can delete — addresses, cards, favorites — must define its miss outcome with orElse instead of trusting the cache.
- Locate crashes by data flow, not screen position. The pay-tap location suggested payments, but the failing step was address resolution one call earlier — read the stack's first Dart frame before paging vendors.
- Audit every firstWhere, first, and last in one pass after an incident. One dangling-id crash means the same optimistic pattern is scattered across the codebase, each waiting for its own stale cache.
| File | Command / Code | Purpose |
|---|---|---|
| io | void main() { | Which Calls Throw and Exactly Why |
| io | class Address { | orElse |
| io | class InboxHeadline extends StatelessWidget { | singleWhere Duplicates and first-last Guards |
| io | class Member { | Nullable Search Patterns for Honest Misses |
| io | class Address { | Stale Caches |
Key takeaways
Common mistakes to avoid
5 patternsCalling firstWhere without orElse
Reading .first or .last without an isEmpty guard
Using singleWhere on unproven-unique data
Trusting cached ids across surfaces
Testing only with populated sample data
Interview Questions on This Topic
What does Bad state: No element mean?
Frequently Asked Questions
20+ years shipping production backend systems. Written from production experience, not tutorials.
That's Flutter. Mark it forged?
5 min read · try the examples if you haven't