skip to content

Sound Null Safety

Sound null safety makes T and T? different types, checked at compile time, with flow-based promotion, ?. and ??, the ! assertion, late and required. Interviewers ask where ! hands back the risk.

part ofDartoverview, primer and where to startread it →
on this pageshow

explore

questions

14

In Dart, what does the late modifier do on a non-nullable variable, and what happens if you read it before assigning it?

level: juniorimportance: must knowfreq 72%

answer

  1. guarantee moves from compile time to runtime
  2. non-nullable type, no initializer
  3. hidden check on reads
  4. has not been initialized
  5. late locals still get flow analysis

basics

~10 s

late lets a non-nullable variable be declared without an initializer and assigned later. Dart checks it at runtime instead, so reading it before any assignment throws a LateInitializationError rather than producing null.

solid answer

~40 s

`late` tells the compiler: this variable has a non-nullable type, I will assign it before I read it, and you may check that at runtime instead of proving it now. It is used where Dart's analysis cannot prove initialisation — fields assigned in a method, top-level variables set in `main`. The type stays non-nullable, so assigning `null` is still a compile error and no `!` is needed at the read. The cost is that the check moves to runtime: reading the variable before any assignment throws an `Error` whose message reads `LateInitializationError: Field '_x' has not been initialized.` For a local variable the analyzer still runs definite-assignment analysis and rejects a read that is *definitely* unassigned.

code

dart · 16 lines
dart
class Report {
  late String title;

  String render() => '# $title';
}

void main() {
  final report = Report();
  try {
    report.render();
  } on Error catch (e) {
    print(e); // LateInitializationError: Field 'title' has not been initialized.
  }
  report.title = 'Q3';
  print(report.render()); // # Q3
}

go deeper

for a junior

Recall that late defers assignment of a non-nullable variable and that reading it too early throws a LateInitializationError, never returning null.

for a middle

Explain which checks move to runtime, why fields and top-level variables need late when locals rarely do, and how late compares with a nullable type plus !.

for a senior

Show you treat every late read as a potential crash site: you use it only where the design guarantees ordering, and you replace it when initialisation depends on runtime events.

for a principal

Frame late as a trade of static proof for runtime checks and set team guidance on when that trade is acceptable, for example lint rules and review expectations.

## Why `late` exists Dart's **sound null safety** means a variable of type `String` can never hold `null`. To keep that promise, the compiler insists that every non-nullable variable is initialised before anyone can read it. For **local variables** it can usually check that by following the control flow. For **instance fields** and **top-level variables** it cannot: whether `heat()` is called before `serve()` depends on how the program runs, and tracking that for every object is intractable. So Dart applies a conservative rule — a non-nullable field needs an initializer or must be set in the constructor's initializer list — and reports `not_initialized_non_nullable_instance_field` otherwise. The `late` modifier is the escape hatch for code that is correct but unprovable: ```dart class Coffee { late String _temperature; void heat() { _temperature = 'hot'; } String serve() => '$_temperature coffee'; } ``` ## What moves from compile time to runtime The Dart documentation's own model is that `late` means **enforce this variable's constraints at runtime instead of at compile time**. Concretely: - The **type stays non-nullable**. Assigning `null` or a `String?` to `_temperature` is still a compile error. - Reads need **no `!`**; the use site looks like any other non-nullable read. - Where the compiler cannot prove the variable is assigned, each **read carries a hidden runtime check**. - If the check finds the variable unassigned, it throws an `Error`, not an `Exception`. The thrown object is the SDK's internal `LateError` from `dart:_internal`. Its `toString()` starts with `LateInitializationError:` and names the variable, for example `LateInitializationError: Field '_temperature' has not been initialized.` or, for a local, `Local 'x' has not been initialized.` Because the class is not public, you cannot write `on LateInitializationError catch` — and you should not want to: an `Error` signals a programming bug to fix, not a condition to handle. ## Where `late` can appear 1. **Instance fields** — the classic case: a value that is set after construction. 2. **Top-level and static variables** without an initializer, for example a configuration object assigned at the start of `main`. 3. **Local variables** — less common, because flow analysis already handles most locals, but useful when assignment happens in a branch the analyzer cannot follow, or for a lazy local initializer. For locals, `late` does not switch analysis off. If the analyzer can see that the variable is **definitely unassigned** at a read, it still reports `definitely_unassigned_late_local_variable`: ```dart void f() { late int x; print(x); // compile-time error: definitely unassigned } ``` If the variable is only *possibly* unassigned, the code compiles and the runtime check guards the read. ## `late` versus a nullable type The alternative to `late String` is `String?` plus `!` at each use. Both fail at runtime when the value is missing, but they say different things to a reader. | | `late String _t;` | `String? _t;` with `_t!` | |---|---|---| | Can hold `null` | never | yes, legitimately | | Assigning `null` | compile error | allowed | | Read before assignment | `LateInitializationError` | `TypeError` from `!` | | Can you test whether it is set? | no API for it | `_t == null` | | Signal to a maintainer | always set before use | absence is a real state | The last two rows matter most. Dart offers **no way to ask whether a `late` variable has been assigned** — no `isInitialized` property exists. Effective Dart therefore says: if you need to check, make the variable nullable and non-`late`, and test for `null`. ## What interviewers are really probing - That `late` does **not** make a variable nullable or give it a default. - That the guarantee is **checked at runtime**, so a mistake becomes a crash, not a compile error. - That the error message names the variable, which is the first clue when diagnosing it. - That `late` is appropriate when initialisation order is genuinely guaranteed by the design, and a smell when it papers over a dependency that may not be ready yet.

  • In Dart, can you test whether a late variable has been assigned yet?
    No. Dart tracks the initialised state internally but exposes no API for it, and reading the variable either runs its initializer or throws. If code needs to ask whether the value exists, Effective Dart recommends making it nullable and non-`late` and checking for `null`, or keeping a separate `bool` when `null` is itself a valid value.
  • Why prefer `late String` over `String?` with `!` at every read when the value is always set before use?
    `late` keeps the type non-nullable, so the compiler still rejects assigning `null`, and reads need no `!`. The declaration documents that absence is not a meaningful state. A nullable type says the opposite: that `null` is a real value callers must handle. Both fail at runtime if the assumption is wrong, so the choice is about stating intent truthfully.

A late variable is a reserved seat with a name card but nobody in it yet: the usher checks the seat each time someone asks for that guest, and raises the alarm if the seat is still empty.

saying these in an interview costs you the question

  • late makes the variable nullable until it is first assigned
  • Reading an unassigned late variable returns null or a default value
  • late is verified entirely at compile time, so it can never crash
  • You can check a late field with an isInitialized property
  • Marking a local late switches off all flow analysis for it
open as a page

In Dart, what do the ?., ?? and ??= operators do, and when is each one the right tool?

level: juniorimportance: must knowfreq 72%

basics

~20 s

In Dart, a?.b reads b only when a is non-null and otherwise yields null; a ?? b yields a unless it is null, evaluating b only then; a ??= b assigns b only when a is currently null.

open as a page

In Dart's sound null safety, what is the difference between String and String?, and what does 'sound' guarantee at run time?

level: juniorimportance: must knowfreq 78%

basics

~20 s

In Dart, String can never hold null while String? holds a String or null, so members like length need a check first. Sound means the guarantee holds at run time: an expression whose static type is non-nullable never evaluates to null.

open as a page

In Dart, what does the postfix ! operator do at run time, and why does it hand the null risk back to you?

level: middleimportance: must knowfreq 64%

basics

~20 s

In Dart, the postfix ! casts a nullable expression to its non-nullable type; if the value is null it throws a TypeError, 'Null check operator used on a null value'. The compiler stops checking, so a wrong assumption becomes a runtime crash.

open as a page

In Dart, why does a null check on a nullable field often not promote it, and how do local copies or private final fields help?

level: middleimportance: must knowfreq 62%

basics

~20 s

In Dart, a public field may be overridden by a getter and a non-final one reassigned between check and use, so flow analysis cannot promote it. Copy it into a local, or since Dart 3.2 make it private and final.

open as a page

In Dart, what does the required modifier on a named parameter guarantee, and how does it interact with nullability and default values?

level: juniorimportance: should knowfreq 58%

basics

~20 s

required makes a named parameter mandatory: a call that omits it is a compile error. It is independent of nullability, so required int? is legal and callers must pass null explicitly, but a required parameter cannot also have a default value.

open as a page

In Dart, how does flow analysis promote a nullable local variable after a null check or an is test?

level: juniorimportance: should knowfreq 60%

basics

~20 s

In Dart, flow analysis tracks every path through a function: after middle != null, or after if (middle == null) return, a local String? is treated as String wherever only non-null values can reach. An is test promotes the same way.

open as a page

In Dart, how does a late final field differ from a plain final field, and what happens if code assigns it twice?

level: middleimportance: should knowfreq 45%

basics

~20 s

A plain final field must get its value from an initializer or the constructor; a late final field without an initializer may be assigned once at any later point. The single assignment is checked at runtime, and a second write throws a LateInitializationError.

open as a page

In Dart, when does the initializer of a late variable declared with one run, and why can a late instance field's initializer use this?

level: middleimportance: should knowfreq 42%

basics

~20 s

Adding late to a variable with an initializer makes it lazy: the initializer runs on the first read, not at construction, and never if nothing reads it. On an instance field it runs after construction, so it may use this.

open as a page

In Dart, what is null-shorting in a ?. chain, and how do ?[], ?.. and callback?.call() follow the same rule?

level: middleimportance: should knowfreq 42%

basics

~20 s

In Dart, when the receiver of ?. is null, the entire rest of the chain is skipped and the expression is null, so a?.b.c needs no second ?. when b is non-nullable. ?[], ?.. and callback?.call() short-circuit the same way.

open as a page

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%

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.

open as a page

A Dart command-line tool intermittently crashes with LateInitializationError on a late service set during startup; how do you diagnose it and redesign so late stops hiding the dependency?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Some path read the late variable before startup assigned it: a command that skips init, an unawaited or failed async setup, or another isolate's own unassigned copy. Fix it by passing the ready service through constructors.

open as a page

In Dart code review, how do you decide between a ?? fallback, a descriptive throw, and ! for a value that is nullable in its type?

level: seniorimportance: should knowfreq 38%

basics

~20 s

In Dart, ask whether null is a normal state: if so, handle it with ?? and a truthful default or a guard; if it means a bug, fail loudly with ?? (throw StateError('...')). Reserve ! for invariants proven right beside it.

open as a page

In Dart, why does a promoted local variable lose its promotion after an assignment, inside a loop, inside a closure or across an await, and how do you restore it?

level: seniorimportance: should knowfreq 25%

basics

~20 s

In Dart, a promotion lasts only while flow analysis can prove no write intervened; a nullable assignment, a write in a loop body, a closure over an assigned variable, or (since 3.13) an await in such a closure demotes it. A fresh final local restores it.

open as a page