Sparround

Common Dart pitfalls

This topic is not about "knowledge" but about reflexes. Interviews often hand you 15 lines of code and ask "what is wrong here?" — and what is being judged is how fast and how systematically you scan.

A useful habit: always read the code in the same order:

1. Is there an await in front of every asynchronous call? 2. How is null treated — is there a !, and what justifies it? 3. Collections: is one mutated while being iterated? Compared with ==? How many times is an Iterable traversed? 4. Numbers: / or ~/? Is money stored in a double? 5. Lifetime: after an await, is the object still alive?

The table below is that list expanded — every row is something that shows up repeatedly in real code reviews.

PitfallWhat goes wrongThe right version
A forgotten `await` (fire-and-forget)The function returns before the work is done; the error bypasses `try/catch` and goes unhandled`await repo.save(x);` — if deliberate, `unawaited(...)` + `catchError`; lint: `unawaited_futures`
Silencing the analyzer with `!`A compile-time error becomes a runtime `Null check operator used on a null value``?.`, `??`, an `if (x != null)` promotion, or `late final` — use `!` only where you can prove it
Mutating a list while iterating it`ConcurrentModificationError` — and it may only appear on certain data`list.removeWhere(...)`, or `for (final x in [...list])` over a copy
Comparing collections with `==``[1, 2] == [1, 2]` is `false`: `==` on a `List` is identity, not deep equality`const DeepCollectionEquality().equals(a, b)` (package:collection); in Flutter, `listEquals`/`mapEquals`
Traversing a lazy `Iterable` twice`map`/`where` re-run on every traversal — work is done twice and side effects repeatMaterialise once: `.toList()` / `.toSet()`
Using `/` for integer division`7 / 2` is `3.5` (a `double`); using it as a list index is a type errorUse `7 ~/ 2` for integer division; keep money as an `int` in minor units
`late` as "we will fill it in later"If read before written you get a `LateInitializationError` — the compiler no longer helpsSet it in the constructor or keep it nullable; `late` is for lazy computation and cyclic setup only
A shadowed variable inside a closureThe local name hides the field — the code looks right but changes nothingUse distinct names or be explicit with `this.count`; `dart analyze` catches some cases
Confusing `==` with `identical``==` compares value, `identical` compares object identity; for strings the results can differAlways `==` for content; `identical` only for cache/canonicalisation questions
Touching a disposed object after an `await`The async work outlives the object: `Bad state: ... after dispose`, or a silent leakAfter every `await`, `if (_disposed) return;`; cancel subscriptions and timers in `dispose()`
Doing money arithmetic with `double``0.1 + 0.2 != 0.3`; rounding errors accumulate and corrupt reportsAn `int` in minor units, or a `Decimal` package; format only at display time
Escaping the type system with `dynamic`All checks move to runtime; `json['user']['name']` eventually throws `NoSuchMethodError``Object?` plus pattern matching, or parsing into a model class; lint: `avoid_dynamic_calls`

Two pitfalls deserve extra explanation, because both fail silently — nothing crashes, the result is just wrong.

Lazy `Iterable`. list.map(f) computes nothing; it returns an object that merely knows how to compute. Every traversal calls f again. So:

  • final r = ids.map(fetchName); print(r.length); print(r.first); calls fetchName for every element (for length) and then once more (for first).
  • If f has side effects (logging, a counter, a network call), those repeat.
  • If you will use the result more than once, call .toList() immediately. Conversely, if you traverse a huge list exactly once, staying lazy saves memory.

Collection equality. In Dart, == on List, Map and Set is identity. That is why [1, 2] == [1, 2] is false, while expect(a, equals(b)) passes in a test (the matcher compares element by element) — an inconsistency that trips a lot of people.

For deep comparison use DeepCollectionEquality from package:collection, or Flutter's listEquals for flat lists. If you override == in your own class, always override hashCode too — and use Object.hashAll(items) when a collection field is involved. A class breaking that rule shows up as a duplicate in a Set and cannot be found again as a Map key.

Interview tip: in a "what is wrong with this code?" task, narrate your scan order: "First I look at async... there is no await here. Then at null... there is a ! with no visible justification. Then at the collections..." Even if you miss one defect, the interviewer sees a systematic reader — and that is valued above the raw count of bugs found. Always finish with prevention: "half of these are caught in CI by the unawaited_futures, avoid_dynamic_calls and always_declare_return_types lints" — that one sentence moves you from a candidate who writes code to one who thinks about the team.

📚 Sources and documentation