Choosing How to Initialize Non‑Null Fields in Dart Sound Null Safety
Guide to picking the right way to declare a non‑null field in Dart sound null safety: compare late final, nullable getters, and required constructor parameters.
20 May 2026, 23:47 UTC

Problem and takeaway
When a class needs a field that is guaranteed to be non‑null after construction but whose value cannot be supplied directly in the field initializer, you must decide how to declare it under Dart’s sound null safety. The goal is to keep the field non‑null for callers, avoid unnecessary runtime checks, and make the initialization contract clear.
Use required constructor parameters when the value is known at object creation; otherwise use late final if you can guarantee the field is set before any read; avoid nullable fields with throwing getters unless you are interfacing with legacy code that cannot be changed.
Decision constraints
- The field must be readable as a non‑null type (
T) from outside the class. - Initialization may happen after the constructor runs (e.g., after an async lookup).
- We want zero‑overhead access in production builds.
- The solution should be clear to static analysers and readers.
Supported options
| Approach | Declaration | When it is set | Access cost | Risk if accessed too early |
|---|---|---|---|---|
late final | late final T _field; | In constructor, a method, or later | Direct field read (no check) | Throws LateInitializationError |
| Nullable with throwing getter | T? _field;T get field => _field ?? throw StateError('field not set'); | Any time before first read | Getter includes null check | Throws StateError (or similar) |
required parameter | final T field;Constructor: ClassName({required this.field}) | At object construction | Direct field read (no check) | None – missing argument is a compile‑time error |
Trade‑off summary
- late final: Zero‑overhead reads after initialization, but you must guarantee the field is set before any use; otherwise a runtime error occurs that can be hard to trace in asynchronous flows.
- Nullable getter: Works even if the field is set later, but adds a null check on every read and throws a custom error if accessed too early, which sidesteps static null‑safety guarantees.
- required parameter: Moves the responsibility to the caller; the field is guaranteed non‑null after construction with no runtime cost. The downside is a more verbose constructor and the inability to defer initialization.
Concrete implementation
The following class demonstrates all three strategies for a field that holds a user‑provided identifier.
class UserProfile {
// 1. Required parameter – value known at construction.
final String userId;
// 2. late final – value supplied later, e.g., after an async load.
late final String _displayName;
String get displayName => _displayName;
// 3. Nullable with throwing getter – for legacy interop.
String? _legacyEmail;
String get email => _legacyEmail ?? throw StateError('email not set');
UserProfile({required this.userId});
// Example of initializing the late field after construction.
Future<void> loadDisplayName() async {
// Simulate async work.
await Future.delayed(const Duration(milliseconds: 100));
_displayName = fetchNameFromServer(); // Assume this returns a non‑null String.
}
// Example of setting the nullable field from legacy code.
void setLegacyEmail(String email) {
_legacyEmail = email;
}
}
String fetchNameFromServer() => 'Alice';
Usage:
final profile = UserProfile(userId: '42');
// userId is immediately available and non‑null.
print(profile.userId); // → 42
// displayName is not set yet; accessing it now would throw LateInitializationError.
await profile.loadDisplayName();
print(profile.displayName); // → Alice (no overhead)
// email remains null until legacy code sets it.
profile.setLegacyEmail('alice@example.com');
print(profile.email); // → alice@example.com
Validation steps
- Enable sound null safety in your project (e.g.,
dart analyze --enable-experiment=non-nullable) and verify that the analyzer shows no potential null‑access warnings for the chosen approach. - Write unit tests that instantiate
UserProfilevia its constructor and assert that each field read does not throw. For thelate finalfield, add a test that readsdisplayNamebefore callingloadDisplayNameand expects aLateInitializationError. - Run the tests in both checked and production modes to confirm that any
assert-based checks (if you added them) are disabled in release, ensuring no runtime overhead in production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.