Skip to main content
A late final variable in Dart is a single-assignment reference whose initialization is deferred until it is first accessed, or whose definite initialization check is deferred from compile-time to runtime. It combines the lazy evaluation and null-safety bypass of the late modifier with the immutability guarantee of the final modifier.

Core Mechanics

  1. Lazy Evaluation: If a late final variable is initialized at the point of declaration, the initializer expression is not executed until the variable is explicitly read.
  2. Deferred Assignment: If declared without an initializer, Dart’s sound null safety compiler allows the variable to remain uninitialized, trusting that it will be assigned exactly once before it is read.
  3. Runtime Enforcement: Because the compiler defers static checks, Dart injects runtime checks. Reading an uninitialized late final variable, or attempting to assign a value to it more than once, throws a LateInitializationError.

Syntax and Behavior

1. Initialized at Declaration (Lazy Evaluation)

When an initializer is provided, the evaluation is deferred. The variable is permanently bound to the result of the expression upon first access.
int _computeValue() {
  print("Evaluating...");
  return 42;
}

void main() {
  // The function _computeValue() is not called when this line executes.
  late final int deferredValue = _computeValue();
  
  print("Before read");
  
  // _computeValue() is executed exactly once here.
  print(deferredValue); 
  
  // Subsequent reads return the cached result without re-evaluating.
  print(deferredValue); 
}

2. Uninitialized at Declaration (Deferred Assignment)

When no initializer is provided, the variable acts as a write-once reference. The compiler permits the omission of an immediate value, but enforces the single-assignment rule at runtime.
void main() {
  late final String writeOnceToken;

  // First assignment succeeds.
  writeOnceToken = "A1B2C3D4";

  // Attempting to read before the first assignment throws LateInitializationError.
  // print(writeOnceToken); 

  // Attempting a second assignment throws LateInitializationError.
  // writeOnceToken = "E5F6G7H8"; 
  
  print(writeOnceToken);
}

Scope-Specific Behaviors

  • Local Variables: Standard local variables are evaluated immediately at the point of declaration. While the late modifier alone is responsible for enabling lazy evaluation for local variables, combining it with final ensures that the lazily computed value (or the deferred assignment) remains strictly immutable after its first initialization.
  • Instance Variables:
    • Deferred Assignment: For class fields, late final allows a non-nullable field to bypass constructor initialization lists. The compiler accepts the class definition without requiring the field to be initialized in the constructor, provided the runtime execution guarantees a single assignment before the field’s getter is invoked.
    • Access to this: When a late final instance variable is given an initializer, that initializer has access to this. It can reference other instance fields or methods because the initialization is deferred until after the object is fully constructed. Standard final field initializers do not have access to this.
class EndpointBuilder {
  final String domain = "example.com";
  
  // Standard final cannot reference 'domain' here.
  // late final permits access to 'this.domain'.
  late final String fullUrl = "https://$domain/api";
}
  • Top-level and Static Variables: In Dart, all top-level and static variables are inherently lazy. Adding late to a final top-level or static variable is redundant if it has an initializer, but it is strictly required if the assignment is deferred to a later point in the execution flow.
Tired of Poor Dart Skills? Fix That With Deep Grasping!Learn More