skip to content

In Dart, how can a non-nullable local or final variable be declared without an initializer, and what does definite assignment analysis check?

level: middleimportance: should knowfreq 35%

answer

  1. every path assigns before read
  2. final can be set in branches
  3. read_potentially_unassigned_final
  4. not for fields or top-level variables
  5. language version 3.9 sharpened it

basics

~20 s

In 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 s

Dart'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 lines
dart
String 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

for a junior

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.

for a middle

Explain definitely versus potentially assigned, how exits and Never calls count, and why loops, closures and try blocks do not provide assignments.

for a senior

Show you use final locals assigned in branches instead of mutable variables or late, and read the analyzer errors to spot a missing path.

for a principal

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.