A Dart service's first unit conversion after startup is slow and sometimes throws from a top-level final lookup table. Why, and how would you fix it?
answer
- globals initialize on first read
- static fields behave the same
- errors surface at the reader
- each isolate has its own copy
- const or explicit warm-up
basics
~20 sDart initializes top-level and static variables lazily, the first time they are read, so the table's costly or failing initializer runs inside the first request, not at startup. Make the table const if it is fixed data, or build it explicitly during startup.
solid answer
~50 sIn Dart, top-level variables and static fields are **lazily initialized**: the initializer runs the first time the variable is read, not when the library loads or `main` starts. A `final factorsToMeters = _loadFactors();` table is therefore built inside whichever request first touches it, adding latency there, and if `_loadFactors` throws, the exception surfaces at that read, far from startup and with the reader's stack. Side effects in the initializer, such as logging or reading files, happen at an unpredictable moment or never. Each isolate has its own copy of top-level and static state, so a worker isolate runs the initializer again on its first read. Fixes: make fixed data `const`, which has no runtime initializer at all; or load it explicitly during startup, fail fast there, and pass it to the code that needs it; at minimum, read the variable once in `main` to move the cost and the failure to startup.
code
dart · 17 lines// Before: runs on the first read, inside a request.
final Map<String, double> factorsToMeters = _loadFactors();
Map<String, double> _loadFactors() {
print('loading factors'); // side effect happens lazily
const raw = {'mi': '1609.344', 'ft': '0.3048', 'in': 'oops'};
return raw.map((unit, text) => MapEntry(unit, double.parse(text)));
}
void main() {
print('started'); // printed before 'loading factors'
try {
print(factorsToMeters['mi']);
} on FormatException catch (e) {
print('first read failed: $e'); // 'oops' is not a double
}
}go deeper
Know that top-level and static variables in Dart are initialized the first time they are read.
Explain which declarations are lazy, which are not, and why const has no runtime initializer.
Diagnose first-request latency and misplaced exceptions from lazy globals, and move loading to explicit startup code.
Decide when shared global state is acceptable in a library versus explicit initialization and injected dependencies.
## The rule: globals are lazy dart.dev states it in one line: **top-level and class variables are lazily initialized; the initialization code runs the first time the variable is used.** "Class variables" means `static` fields. The null-safety guide adds that a `late` instance field with an initializer "works exactly like an initializer on a top-level variable or static field", which is the same lazy mechanism. | Declaration | When its initializer runs | |---|---| | `const x = ...;` (top-level or `static const`) | Never at runtime; the compiler computes it | | `final x = f();` at top level | On the first read of `x` | | `static final x = f();` in a class | On the first read of `x` | | Local `final x = f();` | When execution reaches the declaration | | Instance field `final x = f();` | During construction of each object | There is no class-loading step that triggers static initializers, and no program-wide initialization order; order is simply the order of first reads. ## What went wrong in the service ```dart final Map<String, double> factorsToMeters = _loadFactors(); ``` 1. **Latency moves to the first user.** Parsing or computing the table happens inside the first request that converts a unit, which is why only that request is slow. 2. **Failures move too.** If `_loadFactors` throws, for example a `FormatException` from a malformed entry, the exception is raised at the read, inside the request handler, with a stack trace that starts at the conversion call rather than at startup. A service that should have refused to start instead fails requests. 3. **Side effects become unpredictable.** Logging, metrics or file reads inside the initializer run whenever the first read happens, and never if nothing reads the variable. 4. **Isolates repeat the work.** Isolates do not share memory; each has its own top-level and static variables. A worker isolate that reads `factorsToMeters` runs the initializer again, paying the cost and risking the failure a second time. 5. **Cycles fail in platform-dependent ways.** If two lazy globals read each other during initialization, there is no public `CyclicInitializationError` any more: Dart 3 removed it, and dart.dev's migration notes say null-safe code no longer detects such cycles at runtime, possibly failing with a `StackOverflowError`, although the web development compiler's runtime keeps an internal cycle error. Either way the failure appears at the first read. ## Fixes, best first - **Make it const.** Conversion factors are definitional (a mile is exactly `1609.344` meters), so a `const` map has no runtime initializer, no latency and no failure mode. - **Load explicitly at startup.** If the table really comes from a file or network, write a `loadConversionTable()` function, call it in `main` before serving, let it fail fast, and pass the result to the converter through a constructor. The dependency becomes visible and testable. - **Warm it up.** As a minimal change, read the variable once in `main` (for example `factorsToMeters.length;`) so the cost and any exception happen at startup. The value is still a hidden global. - **Keep initializers pure.** Lazy initializers should compute values, not perform I/O or logging whose timing matters. ## How to spot it in production - **Traces** show one slow request right after each deploy or scale-up, and none afterwards. - **Stack traces** of the failure start at an innocent-looking read of a global, not in any loading code. - **Logs** from the initializer appear in the middle of request handling instead of at startup. - **Code search** for top-level or `static final` declarations whose initializers call functions finds the candidates quickly. Once spotted, the fix is usually small; the diagnosis is the part that needs knowing the rule. ## When laziness is a feature Laziness is not a bug in itself. A rarely used, expensive global costs nothing if it is never read, and command-line tools start faster because unused globals are skipped. The problem is only hidden cost or hidden failure on a path where you expected neither. ## Related but separate Making an *instance* field lazy uses the `late` modifier, which is covered with null safety; spawning isolates and passing data between them is covered with isolates.
- Does a Dart static final field initialize when its class is first used?No. Dart has no class initialization step. A `static final` field initializes on the first read of that field, exactly like a top-level variable. Creating instances or calling other static members does not trigger it.
- Why does a Dart worker isolate pay the initialization cost again?Isolates do not share memory, and each one has its own set of top-level and static variables. The first read inside the worker runs the initializer for the worker's copy. Pass the computed data in the message or closure instead, or make it const.
- How would you test that the conversion table loads correctly?Move loading into an explicit function that returns the table or throws, and test that function directly, including a malformed entry. A lazy global is harder to test because its first read, and so its failure, depends on which test touches it first.
saying these in an interview costs you the question
- Top-level finals are all initialized before main runs
- A static final field initializes when its class is first instantiated
- Every isolate sees the main isolate's already-initialized globals
- A const top-level table is also built lazily at the first read
- Release builds initialize every top-level final eagerly at startup