LateInitializationError: Assign Before You Read
Assign every late field on all paths: LateInitializationError means code read a late variable before assignment.
20+ years shipping production backend systems. Everything here is grounded in real deployments.
- ✓Dart null safety basics and widget lifecycle
- ✓A Flutter screen using StatefulWidget
- ✓flutter test working in your project
- LateInitializationError means your code read a late variable before anything assigned it — the compiler trusted your promise and the runtime caught the gap
- Trace every path to the first read: conditional branches, early returns, and initState ordering are where assignments get skipped
- Prefer required constructor parameters for values known at creation, nullable types for genuinely absent data, and late only for initState setup
- Cover each construction path in widget tests so an unassigned read fails in CI instead of in users' hands
Imagine promising a friend you will bring dessert to the party, and the host lists your cake on the menu. If you show up empty-handed and someone orders a slice, the kitchen crashes — that is LateInitializationError. The late keyword is your promise that you will deliver the value before anyone asks for it. When some path through your code forgets to bake the cake — a skipped branch, an early exit — the first bite request explodes.
Field '_user' has not been initialized. One line, zero helpful context, and your screen is red. LateInitializationError is Dart's way of saying you marked a variable late — promising assignment before first read — and then read it on a path where the assignment never ran. It thrives in StatefulWidgets, where initState, async callbacks, and conditional branches create dozens of paths to the same field.
The keyword feels like boilerplate relief. Non-nullable fields must be assigned by construction, and late seems to waive that rule for values that arrive in initState or from the first event. But late waives nothing — it converts a compile-time guarantee into a runtime check, moving the failure from your editor to your users. Every late field is a promise the runtime will audit exactly once, at the worst moment.
This guide makes the promise explicit. You will see how the check works, find the skipped-assignment paths that cause most incidents, choose correctly between late, nullable, and required constructor parameters, fix initState ordering traps, and write tests that spring every path. By the end, late will feel like the loaded tool it is — powerful in three situations, dangerous everywhere else.
The Promise late Makes and the Check It Inserts
Writing late String name compiles a contract: I will assign name before anyone reads it, and you may skip the compile-time proof. The compiler obliges by inserting a hidden initialized flag alongside the field. Every read checks the flag first — assigned means return the value, unassigned means throw LateInitializationError naming the field. The error message is precise because the runtime knows exactly which promise broke.
This is strictly weaker than normal initialization. A final field with an initializer cannot be wrong; a required parameter cannot be missing; a late field can be both, with the failure deferred to the first read on the worst path. Late trades compile-time certainty for authoring convenience, and the interest on that loan compounds with every branch, early return, and async gap between declaration and read.
Respect the tool by auditing reads, not writes. Authors verify the assignment they wrote; incidents come from the read they forgot on the path they never walked. Search every late field's usages, confirm each read provably follows an assignment on all paths, and convert any field whose reads you cannot enumerate. If you cannot list the readers, you cannot keep the promise.
Skipped Assignments: Branches and Early Returns
Most incidents fit one shape: assignment inside a conditional, read outside it. The monthly branch assigns, the annual branch returns early, and build reads unconditionally — one path keeps the promise, the other breaks it. Early returns are especially treacherous because they look like clean handling while silently skipping every line below them, including your assignment. The compiler stays quiet because late told it not to check.
Guard clauses multiply the risk. Validation returns for empty input, promo checks, feature flags — each return above the assignment creates an unassigned path to every later read. Authors add guards months after the field, never re-auditing the promise. The field that was safe at introduction becomes a minefield one guard clause at a time, with each addition looking harmless in review.
Fix the shape, not just the line. Hoist assignments above all returns and conditionals so every path assigns unconditionally, or convert the field so the compiler enforces it. When reviewing, read initState bottom-up from each return: everything below a return is skipped, and any late assignment down there is a crash waiting for its path. Returns deserve the same scrutiny as the assignments they bypass.
Choosing Between late, Nullable, and Required
Three declarations, three availability stories. Required constructor parameters fit values known at creation: the caller must supply them, the compiler proves every path, and no runtime check exists to fail. When the incident team replaced late Plan with a required parameter, the fatal path became unrepresentable — the strongest possible fix. Default to this whenever the value exists before the widget does.
Nullable types fit genuinely absent data: a user who may not be logged in, an optional avatar, a draft_DISCARD that might not exist. Nullability forces every reader through explicit handling — question marks, conditionals, default branches — so absence is a designed state rather than a crash. Teams that fear null checks end up with late fields that crash instead of UIs that gracefully show signed-out states.
Reserve late for the true gap: values unavailable at construction yet guaranteed before first read. Controllers and animations built in initState, lazily computed caches, and injected test doubles qualify. Even there, prefer late final to prevent reassignment surprises, and document the guarantee in a comment. If you cannot write that comment confidently, you have chosen the wrong declaration.
initState Ordering Traps That Skip the Write
InitState has an order contract developers violate casually. Super.initState runs first so the framework wires the State before your code touches it; helpers called during initState must not read late fields assigned later in the same method; and any read — including through a getter or a child widget built synchronously — before the assignment line throws. The method reads top to bottom like any other, but authors treat it as a bag of setup lines where order does not matter.
DidChangeDependencies adds a second trapdoor. Code moved there for inherited widgets runs after initState but also re-runs on dependency changes — a late field assigned in didChangeDependencies without a guard reassigns (fine for plain late, fatal for late final with Already initialized). Conversely, fields read in didChangeDependencies but assigned in an async callback face the timing gap: the method runs before the Future completes, and the read explodes.
Impose a fixed initState shape: super first, unconditional assignments next, listener registration after, and no reads of your own late fields until assignment. Helpers take values as parameters rather than reading fields. This boring consistency makes ordering incidents structurally impossible instead of merely unlikely.
late final and Lazy Patterns Done Right
Late final is the disciplined half of the keyword: assign exactly once, read many times, with double-assignment throwing Already initialized instead of silently overwriting. Controllers, animation objects, and stream subscriptions built in initState are its natural clients — created once the State exists, immutable thereafter. Single assignment plus immutability removes an entire axis of surprises that plain late permits.
Lazy late fields compute on first read instead of in initState, suiting expensive values some paths never need. Declare late final cache = expensiveComputation(), and paths that never read it never pay. But laziness has its own trap: if computation throws or depends on context that changed since construction, the failure surfaces at an unpredictable read site. Keep lazy computations pure and context-free so first-read timing never matters.
Never use plain late for mutable state that setState updates — reassignment-friendly fields invite read-before-write races that late final would at least constrain. And never expose a late field to other classes: external readers cannot know your assignment timing, so every cross-class late read is someone else's crash. Keep late private, final where possible, and documented with its guarantee. A comment stating when assignment happens turns the promise from folklore into contract.
Tests That Spring Every Path to the Field
Late incidents are path coverage failures, so the test strategy is path enumeration. List every route to the screen's first read of the field — each tab, each plan tier, each promo flag, deep links included — and pump the widget once per combination asserting tester.takeException is null. The matrix for the paywall incident was two tiers times two promo states: four pumps that would have caught the crash in seconds.
Test construction paths too, not just UI paths. States built by tests that skip your usual setup helper reproduce the deep-link crash shape: a State whose initState ran but whose handler never fired. If any construction path leaves the field unassigned at first read, either the path or the declaration is wrong — decide explicitly instead of discovering it in production.
Keep the matrix in CI beside the widget file so new branches extend it. A test named plan_matrix_test fails the moment someone adds a read without an assignment, turning the runtime audit into a build-time gate. Late fields with full path matrices stop generating incidents; late fields without them are just scheduled crashes. Schedule the matrix review alongside every feature branch touching the screen.
Early Return Skipped Assignment, Crashed Paid Onboarding
- Prefer required constructor parameters over late for values known at creation. The compiler then rejects unassigned paths at build time instead of crashing one user segment at runtime.
- Test every branch combination, not just the happy path. A matrix of plan tiers times promo states would have walked the fatal early return in seconds under flutter test.
- Correlate crashes with code paths before vendors. An annual-only pattern can mean a skipped assignment just as easily as a broken SKU — read the stack's first Dart frame before opening provider tickets.
| File | Command / Code | Purpose |
|---|---|---|
| io | class Greeting extends StatefulWidget { | The Promise late Makes and the Check It Inserts |
| io | class PlanCard extends StatefulWidget { | Skipped Assignments |
| io | class PlanBadge extends StatelessWidget { | Choosing Between late, Nullable, and Required |
| io | class OrderedSetup extends StatefulWidget { | initState Ordering Traps That Skip the Write |
| io | class ReportView extends StatefulWidget { | late final and Lazy Patterns Done Right |
Key takeaways
Common mistakes to avoid
5 patternsDeclaring late for values known at construction
Adding guard returns above late assignments
Reading late fields from async-built UI
Exposing late fields to other classes
Using plain late for mutable setState-managed state
Interview Questions on This Topic
What exactly throws LateInitializationError?
Frequently Asked Questions
20+ years shipping production backend systems. Everything here is grounded in real deployments.
That's Flutter. Mark it forged?
5 min read · try the examples if you haven't