skip to content

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