Skip to main content
The late modifier allows the declaration of non-nullable variables that are initialized after declaration but before access, or lazily upon first access. It instructs the Dart analyzer to defer definite assignment checks from compile-time to runtime.

Syntax

The late keyword is placed before the type declaration. It supports two distinct semantic patterns:
  1. Late Initialization: Declaring a variable without an immediate value.
  2. Lazy Initialization: Declaring a variable with an initializer expression.
class DataProvider {
  // Pattern 1: Late Initialization
  // The compiler assumes this will be assigned before usage.
  late String status;

  // Pattern 2: Lazy Initialization
  // The initializer runs only when 'computedValue' is first accessed.
  late int computedValue = _calculateHeavyData();

  int _calculateHeavyData() {
    print("Calculating...");
    return 42;
  }
}

Static Analysis and Definite Assignment

In Dart’s sound null safety system, non-nullable instance variables must usually be initialized at the point of declaration or within the constructor’s initializer list. The late modifier disables this specific compile-time restriction. By marking a field late, you establish a contract with the compiler that the field will be assigned a value before it is read. This satisfies the static flow analysis requirements without an immediate assignment.

Runtime Semantics

While late relaxes compile-time checks, it adds runtime verification.

Uninitialized Late Fields

If a late variable is declared without an initializer:
  1. Setter: Assigning a value functions as a standard assignment.
  2. Getter: Reading the variable triggers a runtime check.
    • If the variable has been assigned, the value is returned.
    • If the variable has not been assigned, a LateInitializationError is thrown.

Lazy Initialized Fields

If a late variable is declared with an assignment:
  1. Deferral: The initializer expression is not evaluated during object construction.
  2. First Access: Upon the first read of the field, the initializer expression executes.
  3. Caching: The result of the expression is stored in the field. Subsequent reads return this cached value without re-evaluating the initializer.
  4. Scope: The initializer expression has access to this, allowing access to other instance members and methods.

Memory Model Implications

Internally, a late field may be implemented using a backing field and a flag to track initialization status.
  • Write: Updates the backing field and marks the flag as initialized.
  • Read: Checks the flag. If uninitialized, it either throws an exception (for uninitialized late fields) or executes the initializer (for lazy fields).

Exception Handling

Failure to uphold the late contract results in a specific runtime exception. Unlike local variables, where static analysis can often detect uninitialized usage at compile-time, instance fields are checked at runtime.
class Runner {
  late String text;
}

void main() {
  final runner = Runner();
  
  try {
    // Reading an instance field before assignment throws LateInitializationError
    print(runner.text); 
  } catch (e) {
    print(e.runtimeType); // Output: LateInitializationError
  }
}
Master Dart with Deep Grasping Methodology!Learn More