Null Safety
Null safety is one of Dart's most important features, introduced in Dart 2.12. It helps prevent null reference errors, which are among the most common programming mistakes.
Understanding Null Safety
In Dart with null safety, variables are non-nullable by default. This means they cannot contain null unless explicitly declared as nullable.
// Non-nullable (default)
String name = 'Alice';
// name = null; // Error: Can't assign null to non-nullable variable
// Nullable (with ?)
String? nullableName = 'Bob';
nullableName = null; // OKNullable Types
Add ? after the type to make it nullable:
int? age; // Can be int or null
String? name; // Can be String or null
List<int>? numbers; // Can be List<int> or nullNon-Nullable Types
Without ?, variables must have a value:
int count = 0; // Must be initialized
String message; // Error: Must be initialized
void main() {
message = 'Hello'; // OK after initialization
}Null-aware Operators
Null-coalescing Operator (??)
Provides a default value if the expression is null:
String? name;
String displayName = name ?? 'Guest';
print(displayName); // Guest
int? score;
int finalScore = score ?? 0;
print(finalScore); // 0Null-aware Assignment (??=)
Assigns a value only if the variable is null:
String? name;
name ??= 'Default';
print(name); // Default
name ??= 'Another';
print(name); // Default (not changed)Null-aware Access (?.)
Safely access properties or methods:
class Person {
String? name;
int? age;
}
Person? person;
// Safe access
print(person?.name); // null (no error)
print(person?.age); // null
person = Person();
person.name = 'Alice';
print(person?.name); // AliceNull Assertion Operator (!)
Asserts that a value is not null:
String? nullable = 'Hello';
String nonNull = nullable!; // Assert non-null
print(nonNull); // Hello
// Throws error if null
String? nullValue;
// String error = nullValue!; // Runtime error!⚠️ Warning: Only use ! when you're absolutely certain the value isn't null.
Late Variables
Use late for variables that will be initialized later but before use:
late String description;
void initialize() {
description = 'Initialized';
}
void main() {
initialize();
print(description); // OK
}
// Late with initializer (lazy initialization)
late String heavyComputation = expensiveOperation();
String expensiveOperation() {
print('Computing...');
return 'Result';
}
void main() {
print('Before');
print(heavyComputation); // Computing... Result
print(heavyComputation); // Result (not computed again)
}Null Safety in Functions
Return Types
// Non-nullable return
String getName() {
return 'Alice';
// return null; // Error
}
// Nullable return
String? findUser(int id) {
if (id == 1) {
return 'Alice';
}
return null; // OK
}Parameters
// Non-nullable parameter
void greet(String name) {
print('Hello, $name');
}
// Nullable parameter
void greetOptional(String? name) {
print('Hello, ${name ?? 'Guest'}');
}
void main() {
greet('Alice');
// greet(null); // Error
greetOptional('Bob');
greetOptional(null); // OK
}Type Promotion
Dart automatically promotes nullable types to non-nullable after null checks:
String? name = 'Alice';
if (name != null) {
// name is promoted to String (non-nullable)
print(name.length); // OK, no need for ?
}
// Using is check
Object? value = 'Hello';
if (value is String) {
print(value.length); // value promoted to String
}Null Safety with Collections
// Non-nullable list
List<String> names = ['Alice', 'Bob'];
// names.add(null); // Error
// Nullable elements
List<String?> nullableNames = ['Alice', null, 'Bob'];
nullableNames.add(null); // OK
// Nullable list
List<String>? maybeList;
print(maybeList?.length); // null
maybeList = ['Alice'];
print(maybeList?.length); // 1Handling Nullable Values
Pattern 1: Provide Default
String? input;
String message = input ?? 'No input';Pattern 2: Null Check
String? name;
if (name != null) {
print(name.toUpperCase());
}Pattern 3: Safe Call
String? name;
print(name?.toUpperCase()); // null if name is nullPattern 4: Assert Non-null
String? name = getName();
print(name!.toUpperCase()); // Use only if certain it's not nullNull Safety Best Practices
- Prefer non-nullable types whenever possible
- Use
latesparingly - only when necessary - Avoid
!unless you're absolutely certain - Use
?.for safe navigation - Provide defaults with
?? - Initialize variables at declaration when possible
- Use type promotion instead of repeated null checks
Migration to Null Safety
If migrating old code:
// Before null safety
String name;
name = null; // Was OK
// After null safety
String? name; // Must be explicitly nullable
name = null; // Now OKComplete Example
class User {
final String id;
final String name;
String? email;
int? age;
User({
required this.id,
required this.name,
this.email,
this.age,
});
String getDisplayName() {
return name;
}
String? getEmail() {
return email;
}
String getEmailOrDefault() {
return email ?? 'no-email@example.com';
}
void printInfo() {
print('ID: $id');
print('Name: $name');
print('Email: ${email ?? 'Not provided'}');
print('Age: ${age ?? 'Not provided'}');
// Safe navigation
print('Email length: ${email?.length ?? 0}');
// Type promotion
if (age != null) {
print('Age in 5 years: ${age + 5}');
}
}
}
void main() {
// Non-nullable fields must be provided
User user1 = User(
id: '1',
name: 'Alice',
email: 'alice@example.com',
age: 25,
);
user1.printInfo();
// Nullable fields can be omitted
User user2 = User(
id: '2',
name: 'Bob',
);
user2.printInfo();
// Null-aware operations
String? searchQuery;
String query = searchQuery ?? 'default';
print('Search query: $query');
// Late variable
late String config;
config = 'Loaded configuration';
print(config);
}