skip to content

How do you make dart analyze fail a CI job on lint findings, and what do its --fatal-infos and --fatal-warnings flags change?

level: middleimportance: should knowfreq 34%

answer

  1. lints report as info by default
  2. exit codes 0 to 3, plus 4
  3. warnings fatal unless --no-fatal-warnings
  4. --fatal-infos makes infos exit 1
  5. or raise one rule under errors:

basics

~20 s

Lints report at info severity, and dart analyze exits 0 on infos unless you pass --fatal-infos (exit 1). Warnings are fatal by default (exit 2), errors always exit 3; alternatively raise chosen rules to warning or error under analyzer: errors:.

solid answer

~40 s

`dart analyze` returns an exit code by the worst finding: `3` for any error, `2` for warnings while `--fatal-warnings` is on (its default; `--no-fatal-warnings` turns it off), `1` for infos only when `--fatal-infos` is passed, otherwise `0`; `4` means the analysis server crashed. Lint rules report at info severity by default, so a plain `dart analyze` in CI prints lint findings and still passes. Two ways to gate: run `dart analyze --fatal-infos` so any lint fails the job, or keep infos non-fatal and map the rules you care about to `warning` or `error` under `analyzer: errors:` in `analysis_options.yaml`. The second is more selective and behaves the same in every IDE, because the severity lives in the file rather than in one CI command.

code

bash · 4 lines
bash
dart analyze                      # lints print, exit 0
dart analyze --fatal-infos        # any lint fails, exit 1
dart analyze --no-fatal-warnings  # warnings print, do not fail
echo $?                           # 0 clean, 1 infos, 2 warnings, 3 errors

go deeper

for a junior

Recall that lints are infos and that plain dart analyze passes on them; --fatal-infos changes that.

for a middle

Explain the exit codes 0 to 3 and 4, the default of each flag, and how analyzer: errors: raises a single rule to a failing severity.

for a senior

Show a gate design that agrees between IDE, local runs and CI, with high-value rules at error and a path to --fatal-infos once the backlog is clear.

for a principal

Weigh strict blocking against developer flow, deciding which rules justify stopping a merge and which stay advisory.

## Severity decides the exit code Every analyzer diagnostic has a **severity**: `info`, `warning` or `error`. Lint rules report as **info** by default; analyzer checks such as `dead_code` or `invalid_null_aware_operator` have their own defaults, and language errors are errors. `dart analyze` prints every finding and then picks an exit code from the worst one it saw, in this order: | Exit code | Meaning | |---|---| | `0` | no findings, or only findings that are not fatal under the current flags | | `1` | info findings, and `--fatal-infos` was passed | | `2` | warning findings, and `--fatal-warnings` is on (the default) | | `3` | at least one error | | `4` | the analysis server crashed, so results may be invalid | The SDK's `dartdev` source computes this directly: errors return `3` unconditionally; then, if warnings are fatal and present, `2`; else, if infos are fatal and present, `1`; else `0`. Info-level `TODO` comments are filtered out of the report and never fail the run. ## The two flags - **`--fatal-warnings`** defaults to **on**. Any warning fails the command with exit code `2`. Pass `--no-fatal-warnings` to report warnings without failing. - **`--fatal-infos`** defaults to **off** and is not negatable. When passed, info findings, which includes every lint at its default severity, fail the command with exit code `1`. This means the common surprise: a project with dozens of lint findings and a CI step that runs plain `dart analyze` **passes**, because none of those infos is fatal. ## Two gating strategies 1. **Flag in CI.** Run `dart analyze --fatal-infos`. Every lint becomes blocking at once. Simple, but all-or-nothing, and a developer running `dart analyze` locally without the flag sees a pass that CI then rejects. 2. **Severity in the options file.** Keep infos non-fatal and raise the rules you really care about: ```yaml analyzer: errors: avoid_print: error unawaited_futures: warning use_build_context_synchronously: error ``` Now those rules fail analysis everywhere: in CI, in a local `dart analyze`, and in the IDE's problems view, while the rest of the lint set stays advisory. It also lets you turn rules on gradually. Teams often combine them: raise the high-value rules to `error` and also pass `--fatal-infos` once the backlog is clean, so no new lint of any kind lands. ## Things that are not what they seem - **A lint set is not a gate.** Including `flutter_lints` makes findings appear; only severity or `--fatal-infos` makes them block. - **`--fatal-infos` does not relax warnings.** With `--fatal-infos` alone, a warning still exits `2`, because `--fatal-warnings` is still on. - **Severity is not compilation.** Raising a lint to `error` fails `dart analyze`, but `dart compile` or `flutter run` still build the code; the analyzer's severities are not read by the compilers. - **`--format`** accepts `default`, `json` and `machine`; the machine and JSON formats are for tools that annotate pull requests, and do not change the exit code. ## Where flutter analyze fits Flutter projects often run `flutter analyze` instead, which wraps the same analyzer with Flutter-specific defaults; its flags belong to the Flutter CLI and are a separate topic. The severity mapping in `analysis_options.yaml` applies to both, which is another argument for putting the gate there. ## A worked CI setup Suppose a Dart package has a backlog of two hundred lint findings, most of them style, plus a handful of `unawaited_futures` and `avoid_print` findings the team considers real bugs. A practical sequence: 1. **Week one**: raise `unawaited_futures` and `avoid_print` to `error` under `analyzer: errors:`, fix those few findings, and keep the CI step as plain `dart analyze`. The job now fails on those two rules and on any warning, while style findings stay advisory. 2. **Following weeks**: clear the style backlog, often with `dart fix --apply` for rules that have automatic fixes, and review what remains by hand. 3. **At zero**: switch the CI step to `dart analyze --fatal-infos`. From then on any new lint of any kind fails the build, and developers see the same result locally because the flag is also in the project's documented check command. ## Reading a failed job When the job fails, the exit code already tells you the class of problem: `3` means an error somewhere (possibly a type error, not a lint), `2` a warning, `1` an info under `--fatal-infos`, `4` a tooling crash that deserves a rerun rather than a code fix. Printing `$?` after the command, or letting the CI runner show it, shortens the diagnosis.

  • Why prefer raising severities in analysis_options.yaml over only passing --fatal-infos in CI?
    The options file is read by every analyzer client, so the IDE, a local `dart analyze` and CI agree on what blocks. It is also selective: you can make a handful of high-value rules errors while the rest of the lint set stays advisory, and tighten further as the backlog shrinks.
  • A job passes --fatal-infos and the code has one warning and no infos; what exit code results?
    `2`. `--fatal-warnings` is on by default and is checked before infos, so the warning decides the result. `--fatal-infos` only adds infos to what fails; it does not change how warnings are treated.

saying these in an interview costs you the question

  • Believes plain dart analyze fails when any lint finding is printed
  • Thinks --fatal-infos turns off the warning check
  • Expects warnings to pass unless --fatal-warnings is added
  • Assumes raising a lint to error stops flutter run from building
  • Treats including a lint set as the same as enforcing it