Dartโs null safety is one of its most important features because it helps prevent one of the most common runtime errors in programming: null reference exceptions (often called โnull pointer errorsโ).
In older versions of Dart, any variable could hold null, and that often led to unpredictable crashes at runtime. With null safety (introduced in Dart 2.12), Dart enforces at compile time that a variable cannot be null unless explicitly declared as nullable.
For example:
int number = 10; // non-nullable
number = null; // โ Compile-time error
But if I explicitly mark it as nullable using a question mark (?), then null becomes allowed:
int? number = 10;
number = null; // โ
Allowed
So the difference is clear โ int can never be null, but int? can.
Now, when I deal with nullable variables, Dart forces me to handle them safely before using them. There are several ways to do that:
- Null check (
!) operator
If Iโm absolutely sure a value isnโt null, I can assert it with!:
int? age;
age = 25;
int validAge = age!; // tells Dart "Iโm sure itโs not null"
But I use this sparingly โ because if age is actually null, it throws a runtime error.
Null-aware operator (?.)
This lets me access a member or call a method safely โ it returns null instead of throwing an exception if the value is null.
String? name;
print(name?.length); // prints null safely
Null-coalescing operator (??)
I use this to provide a fallback value if something is null:
String? username;
String displayName = username ?? 'Guest';
This is especially common in Flutter widgets where some data may be missing.
Null-aware assignment (??=)
This assigns a value only if the variable is currently null:
int? count;
count ??= 0; // assigns 0 only if count is null
In one of my projects โ a Flutter e-commerce app โ I faced a real challenge with nullable API responses. The backend sometimes returned null for optional fields like discount or description. Before null safety, this led to runtime crashes when trying to display these fields directly in widgets. After migrating to sound null safety, I made the model fields nullable:
class Product {
final String name;
final double? discount;
Product({required this.name, this.discount});
}
And while displaying, I safely handled it:
Text('Discount: ${product.discount ?? 0}%')
This eliminated the crashes entirely and made the code self-documented โ I can instantly see which values might be null.
A limitation I initially encountered during migration was the complexity of legacy code โ pre-null-safety packages or models without type annotations needed a lot of refactoring. Dartโs migration tool helped, but manual cleanup was still needed.
Thereโs also the concept of late variables, which are non-nullable but initialized later. I use them when I canโt initialize a variable immediately but will do so before use:
late String token;
void init() {
token = 'abc123';
}
void main() {
init();
print(token); // safe, token is initialized before use
}
However, I need to be cautious โ accessing a late variable before itโs assigned throws a runtime error.
As an alternative for safer handling in asynchronous or optional data scenarios, I sometimes use the maybeWhen or fold pattern from functional libraries like dartz โ they make null-safe handling more declarative.
So in summary โ Dartโs null safety:
- Enforces non-nullable types at compile time.
- Uses
?,!, and null-aware operators to handle nullable values. - Prevents null-related crashes and improves code clarity.
- Encourages defensive and predictable programming.
