| |

Dart 7 🎯 Null Safety Fundamentals: Non-nullable Types, the ? Operator, and Late Initialization (late)

Null safety is one of Dart’s defining features and a major reason the language is suitable for building reliable applications. In older languages, accessing a null value where a non-null was expected was the most common cause of crashes. Dart 3 solved this with sound null safety: types are non-nullable by default, and the compiler forces you to explicitly declare when a variable may be null, then handle that possibility safely before the program runs .

This fundamental shift changes how you think about every variable, parameter, and field in your code. A String cannot be null. An int cannot be null. If you want a value that might be absent, you must write String? or int?. The compiler then tracks every use of that variable and refuses to compile code that might dereference null without a check. The result is that null dereference errors — historically a runtime crash — become edit-time analysis errors you fix before deploying .

This chapter covers the three pillars of Dart’s null safety: non-nullable types (the default), the ? operator (how to declare nullability), and late (how to defer initialization of non-nullable fields when the analyzer cannot prove they are assigned before use).

Key point: Dart types are non-nullable by default. Add ? to allow null. Use late when a non-nullable variable will definitely be initialized before it is read, but the analyzer cannot prove it. The ! operator asserts non-nullness at runtime and throws if the value is null.


Why null safety exists

The null dereference problem. Accessing a property or calling a method on a null value causes a runtime crash. In languages without null safety, this is the single most common source of errors. The program compiles, runs, and then fails when a value that was expected to be present turns out to be absent. Sound null safety eliminates this class of error by making the compiler check every access .

The API documentation problem. Without null safety, there is no reliable way to know from a function signature whether a return value might be null. Comments say “may return null” or documentation notes it, but the type system does not enforce it. With null safety, the type itself is the documentation: String means never null, String? means possibly null, and the compiler enforces both .

The migration problem. Dart was not always null-safe. The language introduced null safety incrementally, allowing mixed-mode projects where some code is null-safe and some is not. This required a sound design that could handle boundaries between safe and unsafe code without allowing null to leak through .

The performance problem. Sound null safety enables compiler optimizations. If the type system knows a variable is non-nullable, it can skip null checks that would otherwise be necessary. Smaller binaries and faster execution are direct consequences of soundness .


a. Non-nullable by default

In null-safe Dart, every variable you declare without ? is non-nullable. It must be initialized with a non-null value, and it can never be assigned null.

int i = 42;              // Inferred as int, never null
String name = 'Alice';   // Never null
final b = Foo();         // Never null

The compiler enforces this. Assigning null to a non-nullable variable is a compile error:

int i = 42;
i = null;  // Error: The value 'null' can't be assigned to a variable of type 'int'

For local variables, Dart uses definite assignment analysis. You can declare a non-nullable local variable without an initial value, but you must assign a value before reading it. The analyzer tracks the flow of your code and rejects any path that might read the variable before assignment .

void main() {
  int x;        // Non-nullable, not yet initialized
  x = 5;        // Assigned
  print(x);     // OK
}

For instance variables and top-level variables, the analyzer is more conservative. It cannot model the flow of your whole application, so a non-nullable field must be initialized in its declaration, in an initializer list, or in the constructor body .


b. The ? operator: declaring nullability

When a variable, parameter, or return type can legitimately be null, add ? to the type.

int? aNullableInt = null;
String? name;
List<int>? numbers;

The ? is a type modifier, not a value. It says “this type includes null.” A String? can hold a string or null. An int? can hold an integer or null.

String? name;              // Can be null
String? name = 'Alice';    // Also valid
String? name = null;       // Explicitly null

Once a type is nullable, the compiler requires you to handle the null case before using the value:

String? name;
print(name.length);  // Error: The property 'length' can't be unconditionally accessed
                     // because the receiver can be 'null'.

You must either check for null or use a null-aware operator:

// Option 1: null check with promotion
if (name != null) {
  print(name.length);  // name promoted to String
}

// Option 2: null-coalescing
print((name ?? 'Guest').length);

// Option 3: null-aware access
print(name?.length);

The ?. operator short-circuits: if the receiver is null, the entire expression evaluates to null instead of throwing. The result type is also nullable .


c. The ! operator: asserting non-nullness

When you know a value is not null but the compiler cannot prove it, you can use the ! operator (the null assertion operator) to force the type to be non-nullable.

String? name = 'Alice';
print(name!.length);  // OK, name is treated as non-null

The ! operator asserts two things to Dart: the value does not equal null, and Dart can assign it to a non-nullable variable . If the value is actually null at runtime, ! throws an exception.

String? name;
print(name!.length);  // Throws: Null check operator used on a null value

The ! operator should be used sparingly. If you are not positive that the value is non-null, use a check or a null-aware operator instead. The ! is a runtime assertion, not a compile-time guarantee. It is appropriate when the flow of the program guarantees non-nullness but the analyzer cannot model it .

A common use case is the Map index operator, which returns a nullable type because the key might not be present. When you know the key exists:

Map<String, int> scores = {'Alice': 95};
int aliceScore = scores['Alice']!;  // Assert non-null

d. The late keyword: deferring initialization

The late keyword solves a specific problem: you have a non-nullable variable that cannot be initialized at its declaration, but you know it will be assigned before it is read.

class IntProvider {
  late int aRealInt;

  IntProvider() {
    aRealInt = calculate();
  }
}

The late keyword has two effects :

  1. The analyzer does not require immediate initialization. A non-nullable field marked late can be declared without a value, and the analyzer trusts you to assign it before use.
  2. The runtime lazily initializes the variable. For a late variable with an initializer expression, the expression is evaluated on first access, not at declaration.
class ExpensiveService {
  late final String config = loadConfigFromDisk();
}

The config is loaded only when config is first read, not when the object is constructed. This is useful for expensive initialization that may never be needed.

The danger of late is that it adds a runtime check. If you read a late variable before assigning it, Dart throws a LateInitializationError .

class BadExample {
  late String value;

  void read() {
    print(value);  // Throws: LateInitializationError
  }
}

The rule is: use late only when you are certain the variable will be initialized before any read. If there is any chance of reading before assignment, use a nullable type instead .


e. The ?? and ?. operators: handling nullable values

The null-coalescing operator ?? provides a default when a value is null.

String? name;
String displayName = name ?? 'Guest';

The null-aware access operator ?. accesses a member only if the receiver is not null.

String? name;
int? length = name?.length;  // null if name is null

These operators compose. A common pattern is to chain ?. and ??:

String city = user?.address?.city ?? 'Unknown';

If any step in the chain is null, the entire expression evaluates to null, and the ?? provides the fallback .

The null-coalescing assignment operator ??= assigns only if the variable is currently null:

String? cache;
cache ??= 'default';
print(cache);  // 'default'

f. Type promotion

Dart’s flow analysis promotes nullable types to non-nullable types within a scope where a null check has been performed. This is called type promotion .

String? name;

if (name != null) {
  print(name.length);  // name is promoted to String
}

After the if (name != null) check, the analyzer knows that within the block, name cannot be null. It treats the variable as String rather than String?, so member access is allowed.

Promotion works with exceptions and return statements as well:

String describe(String? value) {
  if (value == null) return 'Unknown';
  return value.toUpperCase();  // value promoted to String
}

Promotion does not work across async gaps or for fields and top-level variables, because the analyzer cannot guarantee the value has not changed .


Complete Example Session

// ============================================
// PART 1: NON-NULLABLE BY DEFAULT
// ============================================
void main() {
  int i = 42;              // Never null
  String name = 'Alice';   // Never null

  // i = null;             // Error: can't assign null to int
  print('$name: $i');
}
// ============================================
// PART 2: DECLARING NULLABLE WITH ?
// ============================================
void main() {
  int? aNullableInt = null;
  String? name;

  print(aNullableInt);  // null
  print(name);          // null
}
// ============================================
// PART 3: HANDLING NULL WITH IF
// ============================================
String greet(String? name) {
  if (name == null) {
    return 'Hello, Guest!';
  }
  return 'Hello, $name!';  // name promoted to String
}
// ============================================
// PART 4: NULL-AWARE ACCESS ?.
// ============================================
void main() {
  String? name;
  print(name?.length);  // null, no error

  name = 'Alice';
  print(name?.length);  // 5
}
// ============================================
// PART 5: NULL-COALESCING ??
// ============================================
void main() {
  String? name;
  String display = name ?? 'Guest';
  print(display);  // Guest
}
// ============================================
// PART 6: NULL ASSERTION !
// ============================================
void main() {
  String? name = 'Alice';
  print(name!.length);  // 5

  name = null;
  // print(name!.length);  // Throws: Null check operator used on null
}
// ============================================
// PART 7: LATE FOR DEFERRED INITIALIZATION
// ============================================
class IntProvider {
  late int aRealInt;

  IntProvider() {
    aRealInt = calculate();
  }

  int calculate() => 42;
}

void main() {
  var provider = IntProvider();
  print(provider.aRealInt);  // 42
}
// ============================================
// PART 8: LATE WITH LAZY INITIALIZATION
// ============================================
class ExpensiveService {
  late final String config = loadConfig();

  String loadConfig() {
    print('Loading config...');
    return 'default';
  }
}

void main() {
  var service = ExpensiveService();
  print('Before access');
  print(service.config);  // Loading config... then default
}
// ============================================
// PART 9: LATE INITIALIZATION ERROR
// ============================================
class BadExample {
  late String value;

  void read() {
    print(value);  // Throws LateInitializationError
  }
}

void main() {
  var bad = BadExample();
  // bad.read();  // Uncomment to see the error
}
// ============================================
// PART 10: TYPE PROMOTION WITH ??=
// ============================================
void main() {
  String? cached;

  cached ??= 'default';
  print(cached);  // 'default'

  cached ??= 'other';  // No change; cached is not null
  print(cached);  // 'default'
}

These ten parts cover the core null safety features: non-nullable defaults, nullable declarations with ?, handling with if, null-aware access, null-coalescing, null assertion, late for deferred and lazy initialization, the late error, and the ??= operator.


Quick Reference

Type Declarations

DeclarationMeaning
String nameNon-nullable; must be initialized, never null
String? nameNullable; can be null, can be uninitialized
late String nameNon-nullable; initialized before use, runtime checked
late final String nameInitialized once, lazily

Null-Aware Operators

OperatorMeaningExample
?.Access if not nulluser?.name
??Default if nullname ?? 'Guest'
??=Assign if nullname ??= 'Guest'
!Assert non-nullname!.length

Handling Nullable Values

ApproachWhen to Use
if (x != null)You need a branch for null and non-null
x ?? fallbackYou want a default
x?.memberYou want null if x is null
x!You know it is not null
lateField cannot be initialized immediately

Common Errors

ErrorCause
can't be assigned to a variable of typeAssigning null to non-nullable
can't be unconditionally accessedUsing . on nullable
LateInitializationErrorReading late before assignment
Null check operator used on a null value! on null

Best Practices

✅ Do This:

// Non-nullable by default
int count = 0;

// Nullable when genuinely optional
String? middleName;

// Handle null explicitly
String display = middleName ?? 'N/A';

// Use late for fields initialized in constructor
late final Database db;

// Use type promotion
if (user != null) print(user.name);

❌ Don’t Do This:

// Avoid ! without certainty
print(user!.name);  // ❌ crashes if user is null

// Avoid late when nullable is appropriate
late String optional;  // ❌ runtime error if never set

// Avoid ignoring null checks
String? name;
print(name.length);  // ❌ compile error

Common Pitfalls

PitfallWhy It HappensFix
LateInitializationErrorReading late before assignmentUse nullable type or initialize earlier
Null check operator used on null! on a null valueCheck with is or ?. first
Non-nullable field not initializedInstance field has no defaultInitialize in constructor or mark late
Map lookup returns nullIndex operator returns V?Use ?? or ! if key is known present
Type promotion not workingField or top-level variableUse local variable or null check
??= not assigningVariable is already non-nullCheck condition; expected behavior

Real-World Examples

1. Optional Configuration

class Config {
  final String? apiKey;
  Config({this.apiKey});
}

String getKey(Config c) => c.apiKey ?? 'default-key';

2. Nullable Return Type

String? findUserEmail(int id) {
  // Returns null if not found
  return null;
}

3. Null-Aware Chain

String getCity(User? user) {
  return user?.address?.city ?? 'Unknown';
}

4. Late Field in Service

class ApiService {
  late final http.Client client;

  ApiService() {
    client = http.Client();
  }
}

5. Late Lazy Initialization

class Cache {
  late final Map<String, String> _data = _load();
  Map<String, String> _load() => {};
}

6. Null Check with Promotion

void printLength(String? text) {
  if (text != null) {
    print(text.length);  // promoted
  }
}

7. Null Coalescing Assignment

var buffer = StringBuffer();
String? cached;
cached ??= buffer.toString();

8. Map Access with !

Map<String, int> scores = {'Alice': 95};
int score = scores['Alice']!;  // Assert key exists

9. Default Value Pattern

class User {
  final String name;
  User({String? name}) : name = name ?? 'Anonymous';
}

10. Late Final for Immutable Lazy

class Repository {
  late final ApiClient _client = ApiClient();
  ApiClient get client => _client;
}

Visual

Null Safety Type Hierarchy

┌──────────────────────────────────────────────────────────────┐
│  NULLABLE vs NON-NULLABLE                                    │
│                                                              │
│  String (non-nullable)                                       │
│  └── Can only hold a string                                   │
│      Must be initialized before use                          │
│      Compiler allows all String operations                   │
│                                                              │
│  String? (nullable)                                          │
│  └── Can hold a string or null                               │
│      Defaults to null if not initialized                     │
│      Compiler requires null handling before operations       │
│                                                              │
│  late String (non-nullable, deferred)                        │
│  └── Can only hold a string                                   │
│      Must be assigned before read (runtime checked)          │
│      Compiler trusts developer's promise                     │
└──────────────────────────────────────────────────────────────┘

Null Handling Flow

┌──────────────────────────────────────────────────────────────┐
│  WHEN YOU HAVE A NULLABLE VALUE                              │
│                                                              │
│  String? name = getValue();                                  │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  Is name null?                                         │  │
│  └────────────────────────────────────────────────────────┘  │
│       │                                                      │
│       ├── Yes ──▶ Use ?? for default                          │
│       │           name ?? 'Guest'                            │
│       │                                                      │
│       ├── Maybe ──▶ Use ?. for safe access                    │
│       │             name?.length                              │
│       │                                                      │
│       ├── No ──▶ Use ! to assert                             │
│       │           name!.length                                │
│       │                                                      │
│       └── Check ──▶ Use if for branching                      │
│                     if (name != null) { ... }                 │
└──────────────────────────────────────────────────────────────┘

late vs Nullable vs Non-nullable

┌──────────────────────────────────────────────────────────────┐
│  CHOOSING THE RIGHT DECLARATION                              │
│                                                              │
│  Can it be null?                                             │
│       │                                                      │
│       ├── Yes ──▶ String? name;                              │
│       │                                                      │
│       └── No ──▶ Can it be initialized now?                  │
│                      │                                       │
│                      ├── Yes ──▶ String name = '...';        │
│                      │                                        
│                      └── No ──▶ Will it be initialized       │
│                                  before use?                  │
│                                       │                       │
│                                       ├── Yes ──▶ late String │
│                                       │                                                    
│                                       └── No ──▶ Reconsider   │
│                                                  (use String?)│
└──────────────────────────────────────────────────────────────┘

Type Promotion

┌──────────────────────────────────────────────────────────────┐
│  BEFORE PROMOTION                                            │
│                                                              │
│  String? name;                                               │
│  print(name.length);  // Error: receiver can be null         │
│                                                              │
│  ─────────────────────────────────────────                   │
│                                                              │
│  AFTER NULL CHECK (promotion)                                │
│                                                              │
│  if (name != null) {                                         │
│    print(name.length);  // OK: name is promoted to String    │
│  }                                                           │
│                                                              │
│  The analyzer knows that inside the block, name cannot be    │
│  null, so it treats it as String, not String?.               │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Default nullabilityNon-nullable
Nullable syntaxString? name
Non-null assertionname! (throws if null)
Null-aware accessname?.length
Null-coalescingname ?? 'default'
Null-coalescing assignmentname ??= 'default'
Deferred initializationlate String name
Lazy initializationlate final String name = expensive()
Type promotionif (name != null) print(name.length)
Runtime errorLateInitializationError if late read before assignment

Key takeaways:

  • Dart types are non-nullable by default. String means never null; String? means possibly null. The compiler enforces this everywhere .
  • The ? operator declares nullability. Add it to any type to allow null. Without it, the type cannot hold null .
  • Null safety turns runtime crashes into edit-time errors. The analyzer catches null dereferences before the program runs, so you fix them during development rather than in production .
  • The ! operator asserts non-nullness at runtime. Use it only when you are certain the value is not null. If you are wrong, it throws .
  • The late keyword defers initialization. It tells the analyzer “this will be assigned before use.” It adds a runtime check that throws if the promise is broken .
  • Type promotion works inside null checks. After if (name != null), the variable is treated as non-nullable within the block .
  • Map lookups return nullable types. The [] operator returns V? because the key might not exist. Use ! when you know the key is present, or ?? for a default .

Remember: Null safety is not a feature you opt into; it is the foundation of every Dart program. The default is non-nullable, which means most of your variables are guaranteed to hold real values. When a value can legitimately be absent, ? declares that, and the compiler forces you to handle it. late handles the narrow case where a non-nullable field cannot be initialized at declaration but will be assigned before use. The ! operator is an escape hatch for when you know more than the compiler, but it is a runtime assertion, not a guarantee. Used together, these features make null dereference errors a thing of the past for Dart developers.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!