09 · Code Quality (dart analyze / lints)¶
Advanced async patterns showed a missing
await crashing a program with an unhandled exception. This module covers
the tool that catches that mistake — and many others — before the code ever
runs: dart analyze, configured with a lint set.
Enabling a recommended lint set¶
analysis_options.yaml at the project root configures the analyzer.
package:lints ships two starting presets — core.yaml (minimal,
correctness-focused) and recommended.yaml (broader, includes style rules)
— that you extend with include:, then layer your own rules on top.
# analysis_options.yaml
include: package:lints/recommended.yaml
linter:
rules:
- unawaited_futures
- prefer_final_locals
- avoid_print
Seeing it catch a real bug¶
Future<void> risky() async {
await Future.delayed(const Duration(milliseconds: 10));
}
Future<void> main() async {
risky(); // missing await -- flagged by unawaited_futures
var x = 5; // flagged by prefer_final_locals
print(x); // flagged by avoid_print
}
info - Missing an 'await' for the 'Future' computed by this expression. Try adding an 'await' or wrapping the expression with 'unawaited'. - unawaited_futures
info - Local variables should be final. Try making the variable final. - prefer_final_locals
info - Don't invoke 'print' in production code. Try using a logging framework. - avoid_print
3 issues found.
unawaited_futures is the automated version of the missing-await trap
from module 01 — the exact bug that turns
a caught exception into a process crash, caught here at analysis time
instead of discovered in production.
The trap: unawaited_futures only fires inside async functions¶
The lint above only triggers when the discarded-future expression sits
inside an async function body — a plain (non-async) main() calling
the same risky() the same way produces zero lint warnings for it,
even though the bug is identical. The rule's authors made this tradeoff
deliberately: a non-async function calling something that returns a
Future and not waiting for it is a common, often-intentional
"fire-and-forget" pattern (starting background work from synchronous setup
code), so the lint stays narrowly scoped to where it's almost always a
mistake — inside code that's already writing await everywhere else. This
means the lint is a safety net, not a guarantee: reviewing async code by
eye for missing awaits is still worthwhile, especially at a non-async
call site.
dart fix: auto-applying lint corrections¶
Many lints (though not unawaited_futures, which needs a judgment call
about whether the future should actually be awaited or genuinely ignored)
have a mechanical fix the tool can apply for you.
dart fix --dry-run # preview what would change
dart fix --apply # apply all available automatic fixes
prefer_final_locals above is exactly this kind of case —
dart fix --apply would change var x = 5; to final x = 5;
automatically, no manual edit needed.
Deliberately silencing a lint¶
Sometimes a lint is wrong for one specific line — silence it narrowly, with a comment explaining why, rather than disabling the rule project-wide.
Suppressing at the project level in analysis_options.yaml (- avoid_print:
false-style overrides, or excluding whole files) should be rare and
reviewed — a lint disabled globally stops protecting any code, including
future code that would have genuinely benefited from the check.
Cheat sheet¶
| Concept | Meaning |
|---|---|
analysis_options.yaml |
Project-level analyzer/lint configuration |
package:lints/core.yaml |
Minimal, correctness-focused preset |
package:lints/recommended.yaml |
Broader preset, adds style rules |
dart analyze |
Runs the configured checks, reports issues |
unawaited_futures |
Flags a discarded Future inside an async function — catches the missing-await crash |
Lint scoped to async contexts only |
A non-async caller doing the same thing isn't flagged — review those by eye |
dart fix --apply |
Auto-applies mechanical fixes for lints that have one |
// ignore: rule_name |
Silence a lint for one line, with a reason in a comment |
How It Actually Works¶
The Dart linter operates on the same analyzer AST-based infrastructure
that powers dart analyze and your IDE's error squiggles — each lint rule
is a small visitor that walks the parsed syntax tree (not the compiled
output) looking for a specific pattern, which is why lints run instantly
without executing your code at all and can catch issues even in code paths
that never run during testing.
unawaited_futures only firing inside async functions is a direct
consequence of how the rule is scoped: it specifically flags a
Future-returning expression statement (a call whose result is discarded)
written inside a function itself marked async, on the theory that if
you're already in async-aware code, forgetting an await is very likely a
mistake rather than intentional fire-and-forget. A plain synchronous
function calling an async function and discarding its Future doesn't
trigger the same lint, because the rule's heuristic is "you were clearly
thinking about async control flow here (you marked the function async)
and still dropped this one" — outside that context, discarding a Future
looks intentional often enough that the lint would generate too many false
positives to be useful, so the analyzer team scoped it narrowly.
dart fix works by having each lint rule optionally ship a matching
code-transform (a mechanical AST rewrite, similar in spirit to a
generator's output but modifying your existing source in place instead of
producing new files) — it's why dart fix --apply can only auto-correct
lints that have an unambiguous mechanical fix (adding await, removing
dead code) and cannot auto-fix lints whose correction requires
understanding intent (like prefer_final_fields when reassignment is
actually load-bearing elsewhere).
Exercise¶
Add avoid_dynamic_calls and always_declare_return_types to your
analysis_options.yaml's rule list. Write a small file that violates both
(a function with no declared return type, and a call through a dynamic-
typed variable), run dart analyze to confirm both are flagged, then fix
them by hand. Finally, add one deliberate, justified // ignore: comment
for a lint you believe is a false positive in that specific spot, and write
a one-sentence justification as the comment text explaining why.