Explain null safety in Dart
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:
| Type | Can be null? | Example |
|---|---|---|
T | ❌ No | String name = 'Alice'; |
T? | ✅ Yes | String? 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
| Situation | Use |
|---|---|
| Field is genuinely optional | String? + handle with ?? / ?. |
You'll initialize later (e.g. initState, DI) | late / late final |
| You're 100% sure it's non-null here | Prefer flow analysis, fall back to ! |
| Value should never have been null | Fix 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
-
What is the
latekeyword and when should you use it?latedefers initialization but promises the variable will be assigned before it's read. Use it for: fields initialized ininitState()/DI, lazy expensive computations (late final), and non-nullable instance fields you can't set in the constructor. The cost: a runtimeLateInitializationErrorif you break the promise. -
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 andfinalfields. Workaround: copy to afinallocal. -
What's the difference between
String?,late String, andlate String??String?is nullable; you must handlenull.late Stringis non-nullable but unset until first write — reading early throws.late String?is nullable and deferred — rarely useful; usually a code smell. -
What does the
!operator actually do? It's a runtime assertion:x!evaluates toxif non-null, otherwise throwsTypeError. It does not change the underlying value or do any casting magic.
How helpful was this content?
Please sign in to rate this article.