Sparround

`late` and initialization

late tells the compiler: "this non-nullable variable is not ready at its declaration, but it will definitely be assigned before the first read — move the check from compile time to runtime."

Technically a late field adds a hidden flag: every read checks "has it been initialised?". If not, a LateInitializationError is thrown. So late does not switch off null safety — it only changes when the check happens.

There are two distinct forms and it matters not to confuse them:

  • `late` without an initializer: late String token; — the value is assigned later (for example in initState or a setup method).
  • `late` with an initializer (lazy): late final config = _loadConfig(); — the expression runs only on the first read and the result is cached. If it is never read, it never runs.

The second form is an excellent tool for expensive objects: heavy parsing, a large RegExp, a crypto key, or a service needed only on some code paths.

DeclarationWhen evaluatedReassignmentRisk
`final String s = ...;`At constructionNoNone
`String? s;`YesA check or `!` at every use
`late String s;`On manual assignmentYes`LateInitializationError`
`late final String s;`On manual assignmentNo — a second assignment is a runtime error`LateInitializationError`
`late final s = compute();`On first read (lazy)NoA side effect at an unexpected time

Field initialization order in a class must be understood to use late correctly. A Dart constructor runs in this sequence:

  • Field initializers (written at the declaration) — at this stage this does not exist yet, so you cannot reference another field or call a method
  • The constructor's initializer list (: x = 1, y = 2) and the super call
  • The constructor body ({ ... }) — here this is available

This produces the most typical correct use of late: when a field depends on another field, a plain initializer will not work because this is not ready. But late final parser = Parser(config); does work, because the expression is evaluated on first read — after the object is fully constructed.

Another frequent use: assigning a late final field in the constructor body (for example late final controller; ... controller = Controller(this);) because a reference to this is required.

When is `late` the right tool, and when does it hide a problem?

Right uses:

  • Lazily computing an expensive value (late final regex = RegExp(...))
  • A field that depends on another field or on this
  • Framework lifecycles: the object becomes ready in a later setup phase rather than in the constructor (Flutter's initState, setUp in tests)
  • Dependency injection: a field wired up once after creation

Uses that hide a problem:

  • The field is genuinely optional — T? is the honest expression, while late turns absence into a crash
  • Writing late to avoid adding a constructor parameter — this merely converts a compile-time check into a runtime crash
  • One class representing several situations — that calls for a sealed hierarchy

A one-sentence criterion: `late` means "later, but definitely"; for "maybe never", use `T?`.

Interview tip. Answering "what does late do?" with "it lets you assign a non-nullable variable later" is only half of it. A strong answer shows the mechanism: "`late` moves the check from compile time to runtime — a hidden flag is added, every read verifies whether it was initialised, and if not a `LateInitializationError` is thrown. With an initializer it becomes lazy and the expression runs only on the first read."

The follow-up is usually: "`late` or `T?` — how do you choose?" Have the criterion ready: "`late` means the value will definitely arrive, just later; `T?` means it may never arrive. Using `late` merely to avoid writing `!` is a mistake, because the outcome is the same crash with a different message."

📚 Sources and documentation