Explain null safety in Dart

High PriorityAsked in ~75% of Flutter interviews

4 min read

Dart Fundamentals

Null safety (Dart 2.12+) makes types non-nullable by default — to allow null, you explicitly opt in with ? (e.g. String?). The compiler then forces you to handle the null case, turning a whole class of runtime crashes into compile-time errors.

It splits every type into two:

TypeCan be null?Example
T❌ NoString name = 'Alice';
T?✅ YesString? nickname;

The compiler tracks nullability through your code (flow analysis), so once you've checked for null, Dart automatically promotes the variable to the non-nullable type.


Code in action

// Non-nullable — must be initialized, never null
String name = 'Alice';
// name = null;                       // ❌ compile error

// Nullable — opt in with ?
String? nickname;                     // defaults to null, that's fine
// print(nickname.length);            // ❌ might be null

// Flow analysis: once checked, Dart promotes the type
if (nickname != null) {
  print(nickname.length);             // ✅ promoted to String here
}

// Early-return promotion works too
void greet(String? n) {
  if (n == null) return;
  print(n.toUpperCase());             // ✅ Dart knows n is non-null
}

The 5 operators you'll use daily

String? name;

name?.length          // 1. ?.   safe access     → null if name is null
name ?? 'Unknown'     // 2. ??   default value   → 'Unknown' if null
name ??= 'Default';   // 3. ??=  assign if null
name!.length          // 4. !    assert non-null → CRASHES if null (use sparingly)

late String later;    // 5. late  "I'll set it before I use it"
later = 'Hello';      //         non-nullable without immediate init

When to reach for which

SituationUse
Field is genuinely optionalString? + handle with ?? / ?.
You'll initialize later (e.g. initState, DI)late / late final
You're 100% sure it's non-null herePrefer flow analysis, fall back to !
Value should never have been nullFix the data model, don't paper over with !

Common mistakes to avoid

// ❌ Bang-operator abuse — turns null-safety off
final user = getUser()!;              // crashes at runtime if null

// ✅ Handle it
final user = getUser() ?? defaultUser;

// ❌ Making everything nullable "just in case"
class User {
  String? firstName;                  // is this really optional?
  String? lastName;
}

// ✅ Be intentional
class User {
  final String firstName;             // required
  final String? middleName;           // truly optional
  final String lastName;              // required
}

// ❌ Using `late` to silence errors you don't understand
late String token;                    // crashes with LateInitializationError if read first

// ✅ Use `late` only when you can guarantee assignment before access

// ❌ Re-checking after a method call breaks promotion
if (user.name != null) {
  someAsyncCall();
  print(user.name.length);            // ❌ Dart can't trust this anymore
}

// ✅ Copy to a local
final n = user.name;
if (n != null) print(n.length);       // ✅ local stays promoted

Interview follow-ups

  1. What is the late keyword and when should you use it? late defers initialization but promises the variable will be assigned before it's read. Use it for: fields initialized in initState()/DI, lazy expensive computations (late final), and non-nullable instance fields you can't set in the constructor. The cost: a runtime LateInitializationError if you break the promise.

  2. Why doesn't if (user.name != null) always promote the field? Class fields aren't promoted because another thread/method could change them between the check and the use. Promotion only works on local variables and final fields. Workaround: copy to a final local.

  3. What's the difference between String?, late String, and late String?? String? is nullable; you must handle null. late String is non-nullable but unset until first write — reading early throws. late String? is nullable and deferred — rarely useful; usually a code smell.

  4. What does the ! operator actually do? It's a runtime assertion: x! evaluates to x if non-null, otherwise throws TypeError. It does not change the underlying value or do any casting magic.

How helpful was this content?

Please sign in to rate this article.