Lesson 14 of 51 intermediate

Null Safety

Eliminating the Billion-Dollar Mistake in Dart

Open interactive version (quiz + challenge)

Real-world analogy

Null safety is like a postal system that guarantees delivery. Before null safety, sending a letter (calling a method) might go to an empty house (null object) and you would never know until things blew up. With null safety, the post office checks every address before sending. The question mark (?) means 'this address might be empty,' the exclamation mark (!) means 'I guarantee someone lives here,' and late means 'someone will move in before the mail arrives.' In team_mvp_kit, null safety prevents crashes when API responses have missing fields.

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

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. 1. Define UserProfile class with a mix of required and optional fields
  2. 2. id is required and non-nullable -- every user must have an ID
  3. 3. name is required and non-nullable -- every user must have a name
  4. 4. email is required and non-nullable -- every user must have an email
  5. 5. avatarUrl is nullable -- not every user uploads a profile picture
  6. 6. bio is nullable -- users may or may not write a bio
  7. 7. lastLoginAt is nullable -- new users who never logged in have no timestamp
  8. 8. Const constructor with required for non-nullable fields and optional for nullable ones
  9. 9. displayName getter simply returns the name
  10. 10. avatarOrDefault getter uses ?? to fall back to a generated avatar URL if avatarUrl is null
  11. 11. bioExcerpt uses the ?. operator to safely call substring on bio, with ?? for a fallback message
  12. 12. isRecentlyActive first checks if lastLoginAt is null and returns false early
  13. 13. If not null, it uses ! to assert non-null (safe because we just checked) and calculates the time difference
  14. 14. Returns true if the user was active in the last 7 days
  15. 15. The describe method uses StringBuffer for efficient string building
  16. 16. Always includes name and email since they are non-nullable
  17. 17. Uses if (bio != null) to conditionally add the bio line
  18. 18. Uses Dart 3 pattern matching with if-case to extract a non-null lastLoginAt into a local variable
  19. 19. Returns the built string

Spot the bug

String getGreeting(String? name) {
  String greeting = 'Hello, ' + name + '!';
  return greeting;
}
Need a hint?
What happens when you try to concatenate null with a string?
Show answer
The + operator cannot be used with a nullable String?. The variable name might be null, and null + '!' would crash. Fix it by using the null coalescing operator: String greeting = 'Hello, ${name ?? "Guest"}!'; or by checking for null first with an if statement.

Explain like I'm 5

Imagine you have a lunchbox. Without null safety, you might open it expecting a sandwich and find nothing -- oops, you go hungry and your day is ruined. With null safety, lunchboxes come in two types: a guaranteed lunchbox (String) that always has food, and a maybe lunchbox (String?) that might be empty. If you have a maybe lunchbox, the rules say you must check inside before trying to eat. The question mark is like a label that says 'check first!' This way, you never bite into nothing.

Fun fact

Tony Hoare, the inventor of the null reference in 1965, called it his 'billion-dollar mistake' because of the countless crashes, security vulnerabilities, and debugging hours it has caused across all programming languages. Dart is one of the few languages that retrofitted sound null safety into an existing language -- most languages with null safety were designed that way from scratch.

Hands-on challenge

Create a class ContactCard with: name (String, required), phone (String?, optional), email (String?, optional), and address (String?, optional). Add a getter completeness that returns a percentage (0-100) based on how many optional fields are filled. Add a method summary() that uses null-aware operators to build a description string, showing 'Not provided' for any null fields. Test with a fully filled contact and a minimal one.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter & Dart