skip to content

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%

answer

  1. late plus initializer
  2. deferred to the first read
  3. may never run at all
  4. runs after construction finished
  5. no await in a late local initializer

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.

solid answer

~40 s

A plain instance field initializer runs while the object is being constructed, before the constructor body, so it cannot touch `this`. Marking the field `late` changes that: the initializer is deferred and evaluated the **first time the field is read**, by which point construction has finished, so the expression may call methods and read other fields. If the field is never read, the initializer never runs, which is useful for expensive values that are not always needed. It is the same lazy behaviour top-level and static variables have by default, which is why `late` on an initialised static is flagged by the `unnecessary_late` lint. Two limits: a `late` local's initializer cannot contain `await`, and a `late final` field with an initializer cannot be assigned at all.

code

dart · 19 lines
dart
class Config {
  Config(this.raw) {
    print('constructed');
  }
  final String raw;

  late final List<String> parts = _split();

  List<String> _split() {
    print('splitting');
    return raw.split(',');
  }
}

void main() {
  final c = Config('a,b,c'); // prints: constructed
  print(c.parts.length);     // prints: splitting, then 3
  print(c.parts.length);     // prints: 3 (cached)
}

go deeper

for a junior

Remember that late plus an initializer means the expression runs on first read, and that this is what lets it use this.

for a middle

Explain eager versus lazy initialisation order, why top-level and static variables are already lazy, and the await and const-constructor restrictions.

for a senior

Judge where the first-read cost and any side effects will land, and choose between a lazy field, a getter and an eager field accordingly.

for a principal

Weigh lazy caching against predictability in shared types; hidden first-read costs and staleness are the tradeoffs a code standard should name.

## Eager and lazy initializers An **initializer** is the expression after `=` in a variable declaration. For an ordinary instance field it is **eager**: Dart evaluates it while allocating the object, before the constructor's initializer list and body run. At that moment the object is not complete, so an instance field initializer **cannot reference `this`** — no calls to instance methods, no reads of other instance fields. Adding `late` to a field that has an initializer turns it into a **lazy initializer**: - The expression is **not** evaluated during construction. - It is evaluated on the **first read** of the field. - The result is stored, so later reads return the cached value. - If nothing ever reads the field, the expression **never runs**. ```dart class Catalog { Catalog(this.items); final List<String> items; late final Map<String, int> index = _buildIndex(); Map<String, int> _buildIndex() => {for (var i = 0; i < items.length; i++) items[i]: i}; } ``` ## Why `this` becomes available Because the lazy initializer runs at the first read, and a read can only happen on an object that already exists, the expression runs against a **fully constructed** instance. The Dart team calls this an extra bonus of lazy fields: you can access `this`, call methods and read other fields. In the example above, `_buildIndex()` reads `items`, which a non-late initializer could not do. ## Where laziness is already the default Top-level variables and static fields with initializers are **implicitly lazy** in Dart: their initializer runs on first access. Writing `late` on them adds nothing, and the `unnecessary_late` lint reports it. The explicit modifier matters for **instance fields** and **local variables**, which are otherwise eager. A lazy **local** is occasionally handy: ```dart String describe(bool verbose, Report r) { late final details = r.expensiveSummary(); return verbose ? '${r.title}: $details' : r.title; } ``` `expensiveSummary()` runs only on the verbose path. ## Rules and limits | Declaration | Initializer runs | Can be assigned later? | |---|---|---| | `int a = f();` (instance field) | during construction | yes | | `late int b = f();` | first read, unless assigned first | yes | | `late final int c = f();` | first read | no — compile error | | `static int d = f();` | first access (implicitly lazy) | yes | Further rules the analyzer enforces: 1. A `late` local's initializer **cannot contain `await`** (`await_in_late_local_variable_initializer`), because the expression may run later, outside the async flow that declared it. 2. A class with a generative `const` constructor **cannot have a `late final` field** (`late_final_field_with_const_constructor`). 3. For a non-final `late` field with an initializer, **writing before the first read** stores the written value and the initializer is never evaluated. ## Costs and pitfalls Laziness moves work, it does not remove it. - The **first reader pays**. If that first read happens in a latency-sensitive spot, the cost lands there. - **Side effects run at an unpredictable time.** An initializer that reads a clock, logs, or opens a file does so whenever the first read happens, which may differ between runs. - **Re-entrancy is an error.** If a `late final` initializer indirectly reads the same field and that nested read completes the initialisation, the outer evaluation then finds the field already set and throws a `LateInitializationError` saying the field `has been assigned during initialization`. - Captured state is read **at first access**, not at construction, so if other fields change in between, the cached value reflects their state at that moment. ## When to use it Use a lazy `late` field when a value is derived from the object itself, is costly, and is not always needed — an index, a parsed form of raw input, a formatter configured from other fields. Prefer a plain getter when the value is cheap or must always reflect current state, and prefer an eager field when the cost should be paid predictably at construction.

  • In Dart, why is writing late on a top-level variable that has an initializer pointless?
    Top-level variables and static fields with initializers are already lazy: Dart evaluates the initializer on first access. Adding `late` changes nothing, and the `unnecessary_late` lint flags it. The modifier only changes behaviour for instance fields and locals, whose initializers are otherwise eager.
  • In Dart, when should a lazy late field be a getter instead?
    When the value must track the object's current state or is cheap to compute. A lazy `late final` field caches the first result, so if the fields it reads change later, it goes stale. A getter recomputes on every call, which is correct for derived values that change and fine for cheap ones.

saying these in an interview costs you the question

  • A late field's initializer runs in the constructor like any other field
  • Instance field initializers can always call instance methods
  • A late final field with an initializer can be reassigned once later
  • Laziness removes the cost of an expensive initializer
  • A late local initializer can await a Future