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.
| Pitfall | What goes wrong | The 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 repeat | Materialise once: `.toList()` / `.toSet()` |
| Using `/` for integer division | `7 / 2` is `3.5` (a `double`); using it as a list index is a type error | Use `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 helps | Set it in the constructor or keep it nullable; `late` is for lazy computation and cyclic setup only |
| A shadowed variable inside a closure | The local name hides the field — the code looks right but changes nothing | Use 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 differ | Always `==` 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 leak | After 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 reports | An `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);callsfetchNamefor every element (forlength) and then once more (forfirst).- If
fhas 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
- Effective Dart: Usageofficialdart.dev