Dart's Sound Null Safety: From Runtime Crashes to Compile-Time Guarantees
Dart's sound null safety turns null-reference crashes into compile-time errors. This post walks through a JSON parsing example, shows how nullable types and the required keyword work, and outlines practical migration steps.
22 Apr 2026, 21:08 UTC

The Problem: Null References at Runtime
Most Dart developers have seen this stack trace: NoSuchMethodError: The method '...' was called on null. It appears in production, often from a code path that worked fine in testing. The root cause is simple: a variable held null when the code assumed a real object. Before Dart 2.12, the type system treated null as a valid value for every type, so the compiler could not warn you.
What Sound Null Safety Changes
Sound null safety makes non-nullability the default. A variable declared as String can never hold null; if you need a nullable string you write String?. The compiler tracks nullability through control flow, so after a null check the type narrows automatically. Because the guarantee is sound, the runtime can optimize based on it — no hidden null checks inserted by the VM.
Worked Example: Parsing JSON into a User Model
Consider a function that decodes a JSON map into a User object. The JSON may omit optional fields.
class User {
final String id;
final String name;
final String? email; // nullable field
final String? phone;
User({
required this.id,
required this.name,
this.email,
this.phone,
});
}
User parseUser(Map json) {
// id and name are required in the source
final String id = json['id'] as String;
final String name = json['name'] as String;
// email and phone may be missing or null
final String? email = json['email'] as String?;
final String? phone = json['phone'] as String?;
return User(id: id, name: name, email: email, phone: phone);
}
With null safety enabled, the compiler forces you to decide: is email truly optional? If the API guarantees it, change the field to String email and handle the missing case explicitly:
final String email = (json['email'] as String?) ?? '[contact removed]';
If you mistakenly write final String email = json['email'] as String; and the key is absent, the cast throws at runtime — but the analyzer already warns that json['email'] returns dynamic?, so the cast is unsafe. You must either accept String? or provide a fallback.
Trade-offs and Migration Friction
- Migration effort: Large codebases need explicit nullability annotations. The
dart migratetool handles straightforward cases, but complex generics (e.g.,List>) often require manual review. - The bang operator (
!): Writingvalue!tells the compiler "trust me, this isn't null." Overusing it re-introduces the very runtime crashes null safety eliminates. Reserve it for cases where you have a logical guarantee the analyzer cannot see (e.g., after a custom validation function). - Late variables:
late String token;defers initialization but throws if accessed before assignment. Useful for dependency injection or lazy fields, but it moves the null check to first access rather than eliminating it.
Actionable Adoption Steps
- Create a new project with
dart create --null-safety my_appto see a clean baseline. - Run
dart analyzeon your existing codebase; fix errors incrementally, starting with leaf libraries. - Enable
strict-castsandstrict-raw-typesinanalysis_options.yamlto catch unsafe patterns early. - Replace
!with proper null checks or??fallbacks wherever the analyzer flags them. - Run your test suite after each migration batch; null safety often reveals hidden bugs in tests themselves.
Once migrated, you gain compile-time confidence that null-reference exceptions are impossible in safe code — a tangible reduction in production incidents.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.