In Dart, how can a non-nullable local or final variable be declared without an initializer, and what does definite assignment analysis check?
answer
- every path assigns before read
- final can be set in branches
- read_potentially_unassigned_final
- not for fields or top-level variables
- language version 3.9 sharpened it
basics
~20 sIn Dart, a non-nullable local may be declared without a value as long as flow analysis proves it is assigned on every path before any read. A final local may be assigned once in each branch. Fields and top-level variables get no such analysis.
solid answer
~50 sDart's **definite assignment analysis** tracks, for each local variable and each point in a function, whether it has definitely been assigned, definitely not, or maybe. A non-nullable local such as `String greeting;` may skip its initializer if every path to each read assigns it first; a read on a path that might skip the assignment is a compile-time error. The same analysis makes `final` locals flexible: `final String label;` can be assigned in each branch of an `if` or `switch`, and the compiler checks it is assigned exactly once on every path — a read before assignment reports `read_potentially_unassigned_final`, a second assignment is an error too. It works only for locals: top-level variables and static fields need an initializer, and instance fields must be set before the constructor body. Exits such as `return`, `throw` and `Never` calls count, so a path that throws needs no assignment.
code
dart · 18 linesString greeting(String first, String? middle, {required bool formal}) {
final String name;
if (middle == null) {
name = first;
} else {
name = '$first $middle';
}
final String prefix;
if (formal) {
prefix = 'Dear';
} else if (first.isEmpty) {
throw ArgumentError.value(first, 'first', 'must not be empty');
} else {
prefix = 'Hi';
}
return '$prefix $name'; // both finals definitely assigned on every path that gets here
}go deeper
Recall that a local may be declared without a value if every path assigns it before use, and that final locals can be set in branches.
Explain definitely versus potentially assigned, how exits and Never calls count, and why loops, closures and try blocks do not provide assignments.
Show you use final locals assigned in branches instead of mutable variables or late, and read the analyzer errors to spot a missing path.
Promote code style that leans on the compiler's proofs, such as final-by-default locals, so reviewers need not trace every path by hand.
## What the analysis tracks **Definite assignment** is the part of Dart's **flow analysis** that answers one question for each local variable at each point in a function: *has it certainly been given a value on every path that reaches here?* The possible answers are **definitely assigned**, **definitely unassigned** and **potentially assigned**. The compiler uses the answer in two ways: 1. A **non-nullable** local may be read only where it is definitely assigned. 2. A **final** local may be read only where it is definitely assigned, and assigned only where it is definitely unassigned. ## Non-nullable locals without an initializer Without null safety, an uninitialized variable was silently `null`. With it, a non-nullable local has no default, so Dart instead checks every path: ```dart String greeting(String first, String? middle) { String result; if (middle == null) { result = 'Hi $first'; } else { result = 'Hi $first $middle'; } return result; // OK: assigned on both paths } ``` Remove the `else` branch and the `return` becomes an error (`not_assigned_potentially_non_nullable_local_variable`): one path reaches it with `result` never set. ## final locals set in branches The same proof lets a `final` local be assigned later, once per path: ```dart final String label; if (middle == null) { label = first; } else if (middle.isEmpty) { throw ArgumentError.value(middle, 'middle', 'must not be empty'); } else { label = '$first $middle'; } print(label); // OK: the throwing path never gets here ``` Rules the analyzer enforces: - reading it where it might still be unassigned — `read_potentially_unassigned_final`; - assigning it where it might already be assigned — an error, because `final` means once; - a path that ends in `return`, `throw`, `break`, `continue` or a `Never`-returning call **does not need** an assignment, because it never reaches the read. ## Where the analysis does not apply | Declaration | Rule | |---|---| | local variable, including `final` | definite assignment before each read | | top-level variable, static field (non-nullable) | must have an initializer at the declaration | | instance field (non-nullable) | initializer, initializing formal or initializer list — set before the constructor body | | optional parameter (non-nullable) | must have a default value | Top-level and static variables can be read from anywhere, so no single function's flow can prove them assigned; fields can be read from any method. When you genuinely need to assign a non-nullable field or top-level variable later, that is what `late` is for — a separate feature with a runtime check. ## Loops, closures and try - Assigning inside a `for` or `while` loop body does not count for code after the loop, because the body might run zero times. - An assignment inside a closure does not count for the enclosing function: flow analysis does not know when, or whether, the closure runs. - An assignment in a `try` block does not count in its `catch`, because the throw may have happened before it. ## Version note Language version **3.9** made flow analysis **assume null safety** when computing promotion, reachability and definite assignment, which makes the results more accurate and can surface extra `dead_code` warnings. It applies to packages whose `sdk` lower bound is 3.9 or higher; with a Dart 3.13 toolchain and a current constraint you have it.
- In Dart, why may a non-nullable local skip its initializer when a non-nullable top-level variable may not?A local is visible only inside one function, so flow analysis sees every path to every read and can prove it assigned. A top-level variable can be read from any library at any time, so no function-local proof exists; the declaration itself must give it a value, or it must be `late`.
- In Dart, is a variable assigned only inside a for loop body definitely assigned after the loop?No. The body might execute zero times, so after the loop the variable is at most potentially assigned. Reading a non-nullable or final local there is a compile-time error; assign it before the loop or on the path that leaves it.
saying these in an interview costs you the question
- A non-nullable local must always be initialized where it is declared.
- An unassigned non-nullable local defaults to null until it is set.
- A final local can only be assigned at its declaration.
- Definite assignment also lets instance fields be set later in methods.
- A path that throws must still assign the final variable.