`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 ininitStateor 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.
| Declaration | When evaluated | Reassignment | Risk |
|---|---|---|---|
| `final String s = ...;` | At construction | No | None |
| `String? s;` | — | Yes | A check or `!` at every use |
| `late String s;` | On manual assignment | Yes | `LateInitializationError` |
| `late final String s;` | On manual assignment | No — a second assignment is a runtime error | `LateInitializationError` |
| `late final s = compute();` | On first read (lazy) | No | A 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
thisdoes not exist yet, so you cannot reference another field or call a method - The constructor's initializer list (
: x = 1, y = 2) and thesupercall - The constructor body (
{ ... }) — herethisis 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,setUpin 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, whilelateturns absence into a crash - Writing
lateto 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
sealedhierarchy
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
- Variables: lateofficialdart.dev