Null Safety
Eliminating the Billion-Dollar Mistake in Dart
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Null safety is Dart's type system feature that distinguishes between values that can be null and values that can never be null. Every type is non-nullable by default. You must explicitly opt in to nullability with a question mark (?). The compiler then enforces null checks at compile time, catching potential null errors before your code ever runs. This eliminates the most common source of runtime crashes in programming -- the 'null reference exception' that Tony Hoare famously called his 'billion-dollar mistake.'
Real-world relevance
In team_mvp_kit, null safety is critical for handling API data reliably. A user profile from the backend might have a bio field or it might not. By declaring bio as String?, the type system forces every piece of code that touches bio to handle the null case. Without null safety, a missing bio could crash the app when trying to display it. With null safety, the compiler catches this at build time. The Hive local storage layer also benefits -- cached data might be absent, and nullable types make this explicit.
Key points
- Non-Nullable by Default — In Dart with null safety, variables cannot be null unless you explicitly allow it. A String variable must always contain a string value. This eliminates an entire category of runtime crashes.
- Nullable Types with ? — Add a question mark after the type to allow null. This tells Dart and other developers that the value might be absent and must be handled carefully.
- Null-Aware Access with ?. — The ?. operator calls a method or accesses a property only if the object is not null. If the object is null, the entire expression evaluates to null instead of crashing.
- Null-Aware Coalescing with ?? — The ?? operator provides a fallback value when the left side is null. It is perfect for default values. The ??= operator assigns a value only if the variable is currently null.
- Non-Null Assertion with ! — The ! operator tells Dart you are certain a nullable value is not null right now. If you are wrong, it throws a runtime error. Use sparingly and only when you have a strong guarantee.
- Late Variables — The late keyword lets you declare a non-nullable variable without initializing it immediately. You promise Dart it will be initialized before it is ever read. Useful for variables set in initState or setup methods.
- Null Safety in Collections — Collections can hold nullable values: List means the items can be null. A nullable collection List? means the list itself can be null. These are different concepts.
- Flow Analysis (Smart Promotion) — Dart's type system is smart enough to 'promote' a nullable variable to non-nullable after a null check. Inside an if-block that checks for null, you can use the variable without ? or !.
- Required Nullable Parameters — A parameter can be both required and nullable. This means the caller must explicitly pass a value, but that value is allowed to be null. This pattern forces conscious decisions about null values.
- Null Safety in team_mvp_kit — API responses often have optional fields. Models in team_mvp_kit use nullable types for optional data and non-nullable types with required for mandatory fields. The fromJson factories handle missing keys gracefully.
Code example
class UserProfile {
final String id;
final String name;
final String email;
final String? avatarUrl;
final String? bio;
final DateTime? lastLoginAt;
const UserProfile({
required this.id,
required this.name,
required this.email,
this.avatarUrl,
this.bio,
this.lastLoginAt,
});
String get displayName => name;
String get avatarOrDefault => avatarUrl ?? 'https://ui-avatars.com/api/?name=$name';
String get bioExcerpt => bio?.substring(0, 50) ?? 'No bio yet';
bool get isRecentlyActive {
if (lastLoginAt == null) return false;
final diff = DateTime.now().difference(lastLoginAt!);
return diff.inDays < 7;
}
String describe() {
final buffer = StringBuffer()
..writeln('Name: $name')
..writeln('Email: $email');
if (bio != null) {
buffer.writeln('Bio: $bio');
}
if (lastLoginAt case final login?) {
buffer.writeln('Last login: $login');
}
return buffer.toString();
}
}Line-by-line walkthrough
- 1. Define UserProfile class with a mix of required and optional fields
- 2. id is required and non-nullable -- every user must have an ID
- 3. name is required and non-nullable -- every user must have a name
- 4. email is required and non-nullable -- every user must have an email
- 5. avatarUrl is nullable -- not every user uploads a profile picture
- 6. bio is nullable -- users may or may not write a bio
- 7. lastLoginAt is nullable -- new users who never logged in have no timestamp
- 8. Const constructor with required for non-nullable fields and optional for nullable ones
- 9. displayName getter simply returns the name
- 10. avatarOrDefault getter uses ?? to fall back to a generated avatar URL if avatarUrl is null
- 11. bioExcerpt uses the ?. operator to safely call substring on bio, with ?? for a fallback message
- 12. isRecentlyActive first checks if lastLoginAt is null and returns false early
- 13. If not null, it uses ! to assert non-null (safe because we just checked) and calculates the time difference
- 14. Returns true if the user was active in the last 7 days
- 15. The describe method uses StringBuffer for efficient string building
- 16. Always includes name and email since they are non-nullable
- 17. Uses if (bio != null) to conditionally add the bio line
- 18. Uses Dart 3 pattern matching with if-case to extract a non-null lastLoginAt into a local variable
- 19. Returns the built string
Spot the bug
String getGreeting(String? name) {
String greeting = 'Hello, ' + name + '!';
return greeting;
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Understanding Null Safety (dart.dev)
- Null Safety in Dart (dart.dev)
- Migrating to Null Safety (dart.dev)