Home › Mobile › Bad State: No Element — Guard firstWhere
Beginner 5 min · September 23, 2026

Bad State: No Element — Guard firstWhere

Pass orElse to firstWhere: Bad state No element means a search found nothing.

N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Written from production experience, not tutorials.

Follow
✓ Production
production tested
September 27, 2026
last updated
2,085
articles · all by Naren
Before you start⏱ 9 min
  • ✓Dart collections basics: lists and iterables
  • ✓A Flutter screen that searches a list
  • ✓flutter test running in your project
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • 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
✦ Definition~90s read
What is Bad State?

Dart's Iterable search methods assume success unless told otherwise. FirstWhere scans for the first matching element and throws StateError with Bad state: No element when nothing matches; without orElse there is no other outcome defined. First and last are shorthand for the same assumption on non-empty collections — call them on an empty list and the identical error fires.

★
Imagine asking a librarian for the first book by an author the library does not carry.

SingleWhere goes further, throwing when zero match and a separate Too many elements error when two or more match.

The error is synchronous and total. It throws inside whatever frame or handler called it, unwinding to the nearest try-catch or, in build methods, to Flutter's error widget — the red screen. There is no partial result, no null, no warning: the method's contract says return an element, and with none available the only legal move is throwing.

That harshness is deliberate, forcing callers to confront the miss case instead of propagating nulls silently.

Three defenses cover every call site. The orElse callback defines the miss outcome, converting throws into fallback values your UI already knows how to render. Explicit isEmpty and length guards handle first, last, and indexing before the throw can form.

Nullable search patterns — manual loops or orElse returning a sentinel — express honestly that some searches legitimately find nothing. Together they turn lookup code from optimistic to total: defined behavior on every input, including empty.

Plain-English First

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.

io/thecodeforge/flutter/throwing_lookups.dartDART
1
2
3
4
5
6
7
8
9
10
void main() {
  final List<String> inbox = <String>[];
  // Each line below throws on this input: no element to return.
  // inbox.firstWhere((m) => m.startsWith('Hi')); // Bad state: No element
  // inbox.first; // Bad state: No element
  // inbox.last; // Bad state: No element
  // inbox.singleWhere((m) => m.isNotEmpty); // Bad state: No element
  // Hardened versions appear in the next sections.
  assert(inbox.isEmpty, 'repro input must be empty');
}
📊 Production Insight
A favorites screen called .first on a list the backend legitimately emptied for new users — every single new signup crashed within seconds of onboarding.
🎯 Key Takeaway
FirstWhere, first, last, and singleWhere throw synchronously on misses — recognize the unguarded shapes in review.

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.

io/thecodeforge/flutter/orelse_fallback.dartDART
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Address {
  const Address(this.id, this.label);
  final String id;
  final String label;
}

const Address pickFallback = Address('new', 'Choose an address');

Address resolveDefault(List<Address> saved, String cachedId) {
  // Misses return the picker fallback instead of throwing.
  return saved.firstWhere(
    (Address a) => a.id == cachedId,
    orElse: () => pickFallback,
  );
}

void main() {
  final Address got = resolveDefault(const <Address>[], 'deleted-id');
  assert(got.id == 'new', 'misses must route to the picker');
}
📊 Production Insight
The checkout hotfix was a three-line orElse that routed stale caches to the address picker — crash category closed in one deploy with zero UI redesign.
🎯 Key Takeaway
Give every firstWhere an orElse whose fallback keeps the user moving, and enforce the rule across the codebase.

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.

io/thecodeforge/flutter/guarded_reads.dartDART
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'package:flutter/material.dart';

class InboxHeadline extends StatelessWidget {
  const InboxHeadline({super.key, required this.messages});
  final List<String> messages;

  @override
  Widget build(BuildContext context) {
    // Guard directly above the read: empty renders, never throws.
    if (messages.isEmpty) return const Text('Inbox zero. Enjoy the calm.');
    final String latest = messages.first;
    final String oldest = messages.last;
    return Text('Latest: $latest / Oldest: $oldest');
  }
}
⚠ singleWhere Promises Uniqueness You Must Prove
Without a uniqueness guarantee at the call site, singleWhere is two crashes waiting — zero matches and duplicates. Prefer firstWhere with orElse unless duplicates genuinely mean corruption.
📊 Production Insight
A coupon screen used singleWhere on codes a bulk import had duplicated — every affected user crashed while the team insisted duplicates were impossible.
🎯 Key Takeaway
Guard first and last with isEmpty, and keep singleWhere only where uniqueness is proven.

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.

io/thecodeforge/flutter/nullable_search.dartDART
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Member {
  const Member(this.id, this.name);
  final String id;
  final String name;
}

// Honest miss: null means absent, and callers must handle it.
Member? findMember(List<Member> members, String id) {
  for (final Member m in members) {
    if (m.id == id) return m;
  }
  return null;
}

int whereMember(List<Member> members, String id) {
  return members.indexWhere((Member m) => m.id == id); // -1 means absent
}

void main() {
  const List<Member> team = <Member>[Member('a', 'Ada')];
  assert(findMember(team, 'zzz') == null, 'misses return null');
  assert(whereMember(team, 'zzz') == -1, 'miss index is -1');
}
📊 Production Insight
A team replaced clever sentinel ids with nullable searches and watched three separate crash clusters resolve into one honest empty-state flow.
🎯 Key Takeaway
Express legitimate misses in types — nullable results or index checks — so callers handle absence by construction.

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.

io/thecodeforge/flutter/cache_reconcile.dartDART
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Address {
  const Address(this.id, this.label);
  final String id;
  final String label;
}

// Reconcile cached ids against fresh data before any lookup runs.
List<Address> reconcile(List<Address> fresh, List<String> cachedIds) {
  final Set<String> live = <String>{for (final Address a in fresh) a.id};
  final List<String> stale = cachedIds.where((String id) => !live.contains(id)).toList();
  for (final String id in stale) {
    dropCachedId(id); // purge the dangling reference once
  }
  return fresh;
}

void dropCachedId(String id) {
  // Delete id from local storage; stubbed for this sample.
  assert(id.isNotEmpty);
}
📊 Production Insight
After the incident, checkout reconciled cached ids on entry and stale-address crashes dropped to zero — users just re-picked an address and paid.
🎯 Key Takeaway
Reconcile cached ids against fresh fetches at flow entry; treat every cached id as falsifiable.

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.

📊 Production Insight
A marketplace added miss-fixture tests to twelve search screens and caught a backend empty-array change in CI — zero user-facing crashes from a deploy that once would have red-screened thousands.
🎯 Key Takeaway
Test empty, miss, and duplicate fixtures per lookup, assert takeException is null, and run them in CI.
● Production incidentPOST-MORTEMseverity: high

Deleted Address Crashed Checkout for 4,100 Users

Symptom
Saturday morning crash reporting showed Bad state: No element erupting from the checkout screen, climbing to 4,100 affected users by Sunday night. Every victim shared a pattern: a default shipping address chosen long ago, later deleted through the web app, with the mobile cache still holding the stale id. Tapping pay crashed instantly and repeatably — retrying never helped because the data, not the network, was the problem. The App Store collected eleven one-star reviews mentioning crashes at payment before the weekend ended.
Assumption
The team assumed a payment-gateway outage because crashes clustered at the pay tap. They spent Saturday checking provider status pages and replaying webhooks that were all healthy. They then assumed a corrupt app update, and prepared a rollback of Friday's release — which contained only copy changes. Both theories fit the pay-step location. The breakthrough came when support noticed every reporter mentioned an old address: the crash fired while resolving the address, one step before any payment code ran.
Root cause
Checkout resolved the cached default address with addresses.firstWhere((a) => a.id == cachedId) and no orElse. Users who deleted that address on the web carried a dangling id in the mobile cache. The filter matched zero records, firstWhere threw synchronously inside build, and the red screen replaced checkout. Data that was merely stale became a hard crash because the lookup assumed its target existed.
Fix
The hotfix added orElse returning a fallback that routes users to the address picker with their cache cleared, shipping Saturday night as 4.3.1 and stopping the crash immediately. The follow-up validated cached ids against fresh fetches on checkout entry, added empty and dangling-id widget tests for every firstWhere call site, and introduced a review rule: no firstWhere without orElse, no first or last without an isEmpty guard. Stale caches now render pickers instead of red screens.
Key lesson
  • 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.
Production debug guideFive steps from the red screen to fallbacks on every lookup.5 entries
Symptom · 01
Red screen with Bad state: No element naming a screen
→
Fix
Open the stack's first Dart frame — it names the exact firstWhere, first, last, or singleWhere call. Reproduce with flutter run by emptying its input: clear the cart, delete the record, or apply the filter that matches nothing. If the crash needs production data, log the list length and predicate inputs just above the call and read them from the next crash report.
Symptom · 02
firstWhere throws but the list looks non-empty
→
Fix
The list has items but none match the predicate — log both. Print the list length and the searched key, then compare: stale cached ids, case mismatches, and type mismatches (int 7 versus String 7) all produce zero-match filters on healthy-looking lists. Fix the comparison or the cache, and add orElse so the next mismatch renders a fallback instead of crashing.
Symptom · 03
singleWhere throws Too many elements instead
→
Fix
Your data holds duplicates the code assumed were unique — print all matches to confirm. Either dedupe upstream with a Set or a unique constraint, or switch to firstWhere with orElse when any-match semantics are acceptable. Reserve singleWhere for data with a proven uniqueness guarantee, and test with duplicated fixtures.
Symptom · 04
first or last throws on an empty list
→
Fix
Guard the read with an isEmpty early return that renders the empty state your designers already made. Search the file for every .first, .last, and index access on the same list — they share the assumption and need the same guard. Verify by launching the screen with zero items and confirming the empty illustration instead of red.
Symptom · 05
You want the whole codebase hardened at once
→
Fix
Search for firstWhere without orElse, bare .first and .last, and singleWhere across lib, then fix each site with a fallback or guard plus a widget test covering empty, miss, and duplicate inputs. Run the suite with flutter test and gate the rule in review so new lookups arrive hardened.
Bad State: No Element Causes at a Glance
Root CauseHow to ConfirmFixPrevention
firstWhere with zero matchesList non-empty but predicate matches nothingAdd orElse returning a momentum-preserving fallbackReview rule: no firstWhere merges without orElse
first or last on an empty listCrash on empty inboxes, carts, or new accountsGuard with isEmpty rendering the empty statePump every list screen with zero items in tests
singleWhere with duplicatesToo many elements error on imported dataDedupe upstream or switch to firstWhere plus orElseTest with duplicated fixtures, not just unique ones
Stale cached id with no live recordCrash follows deletions on another surfaceReconcile ids on flow entry; orElse to pickerRevalidate cached selections on critical-flow entry
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
iothecodeforgeflutterthrowing_lookups.dartvoid main() {Which Calls Throw and Exactly Why
iothecodeforgeflutterorelse_fallback.dartclass Address {orElse
iothecodeforgeflutterguarded_reads.dartclass InboxHeadline extends StatelessWidget {singleWhere Duplicates and first-last Guards
iothecodeforgeflutternullable_search.dartclass Member {Nullable Search Patterns for Honest Misses
iothecodeforgefluttercache_reconcile.dartclass Address {Stale Caches

Key takeaways

1
No-element throws are synchronous
define the miss outcome or the frame dies.
2
Every firstWhere needs orElse with a fallback that preserves user momentum.
3
Guard first, last, and indexing with isEmpty and length checks directly above.
4
Keep singleWhere only where uniqueness is proven; otherwise downgrade deliberately.
5
Reconcile cached ids at flow entry
caches are hypotheses, not truth.
6
Fixture empty, miss, and duplicate inputs per lookup in CI.

Common mistakes to avoid

5 patterns
×

Calling firstWhere without orElse

Symptom
Red screens the first time production supplies a miss — empty carts, cleared filters, deleted records the cache still names.
Fix
Pass orElse with a fallback that keeps the user moving, and gate the rule in review so new call sites arrive hardened.
×

Reading .first or .last without an isEmpty guard

Symptom
Every new user with a legitimately empty list crashes seconds into the relevant screen.
Fix
Check isEmpty directly above the read and render the designed empty state your designers already produced.
×

Using singleWhere on unproven-unique data

Symptom
Too many elements crashes after imports or merges duplicate rows the code assumed were unique.
Fix
Prove uniqueness with a constraint or switch to firstWhere plus orElse when any-match semantics suffice.
×

Trusting cached ids across surfaces

Symptom
Deterministic crashes after web deletions, with retries never helping because the data — not the network — is stale.
Fix
Reconcile cached ids against fresh fetches at flow entry and purge dangling references before lookup.
×

Testing only with populated sample data

Symptom
Miss paths never execute until production, so the first empty inbox or zero-match filter discovers the throw.
Fix
Fixture every lookup with empty, miss, and duplicate inputs in widget tests run under CI.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does Bad state: No element mean?
Q02JUNIOR
How does orElse fix a firstWhere crash?
Q03SENIOR
When is singleWhere the wrong choice?
Q04SENIOR
How do stale caches produce this error?
Q05SENIOR
What test fixtures harden a lookup?
Q01 of 05JUNIOR

What does Bad state: No element mean?

ANSWER
A search method like firstWhere, first, last, or singleWhere found nothing to return. The collection was empty or no element matched, and without an orElse the only legal move is throwing.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Why does the same screen crash for some users only?
02
Should I wrap lookups in try-catch instead?
03
Is orElse expensive when nothing misses?
04
How do I find all unguarded lookups?
05
What about index out of range errors?
06
Can the backend prevent these crashes?
N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Written from production experience, not tutorials.

Follow
✓ Verified
production tested
September 27, 2026
last updated
2,085
articles · all by Naren
🔥

That's Flutter. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
LateInitializationError: Assign Before You Read
6 / 7 · Flutter
Next
CocoaPods Missing? Fix macOS iOS Builds
→