Skip to main content
The late modifier in Dart is applied to variable declarations to alter the language’s sound null safety semantics by deferring initialization until runtime. It instructs the compiler to bypass static initialization requirements for instance and top-level variables. For local variables, Dart still performs flow analysis; however, late relaxes the strict requirement that a variable must be definitely assigned before use. The compiler will only issue a static error if a late local variable is definitely unassigned at the point of access. Otherwise, it defers the initialization check dynamically to the variable’s first access.

Syntax and Scope

The late modifier precedes the type declaration. It can be applied to top-level variables, instance fields, and local variables. It can be used with or without an immediate initializer and combined with the final modifier.
// Top-level variable
late String globalState;

String _readEnvironment() => "production";

class Configuration {
  // Instance field
  late int timeout;
  
  // Initialized instance field (lazy evaluation)
  late String environment = _readEnvironment();
}

void processData(bool condition) {
  // Local variable
  late final bool isValid;
  
  if (condition) {
    isValid = true; 
  }
  
  // Initialization check is deferred to runtime.
  // If condition was false, accessing isValid here throws an error.
}

Core Mechanics

1. Runtime Initialization Checks

When a late variable is declared without an initializer, the Dart runtime tracks the variable’s initialization state. If the compiler cannot prove the variable is definitely unassigned, it allows the code to compile but inserts a runtime check. If the variable is read before a value is assigned, the runtime throws a LateInitializationError.
void execute(bool condition) {
  late String text;

  if (condition) {
    text = "Initialized";
  }

  // Compiles because 'text' is not *definitely* unassigned.
  // Throws LateInitializationError at runtime if 'condition' is false.
  print(text); 
}

2. Lazy Evaluation

When a late variable is declared with an initializer, the evaluation of the initializer expression is deferred. The expression is not executed when the variable comes into scope or when an object is instantiated. It is executed exactly once, at the exact moment the variable is first read.
class Example {
  // _computeValue() is only called the first time 'value' is accessed.
  late int value = _computeValue();

  int _computeValue() {
    return 42;
  }
}

3. Interaction with Nullable Types

The late modifier can be applied to nullable types (e.g., Type?). This combination allows the runtime to distinguish between a variable that is truly uninitialized and a variable that has been explicitly initialized to null.
class NullableExample {
  late String? optionalName;

  void checkName(bool condition) {
    if (condition) {
      optionalName = null;
    }
    
    // If condition is false, reading optionalName throws LateInitializationError.
    // If condition is true, it safely returns null.
    print(optionalName); 
  }
}

4. Interaction with final

The late modifier can be combined with final to create a variable that is both deferred and immutable after its initial assignment.
  • late final without initializer: The variable can be assigned exactly once at runtime. Any subsequent attempt to reassign it will result in a LateInitializationError.
  • late final with initializer: The initializer is evaluated lazily on first access, and the resulting value is permanently bound to the variable.
class Example {
  late final String singleAssignment;

  void initialize() {
    singleAssignment = "First"; // Valid on first execution
  }

  void reinitialize() {
    // Throws LateInitializationError if initialize() was already called
    singleAssignment = "Second"; 
  }
}

5. Instance Context Access in Initializers

Because late field initializers are evaluated lazily after instance construction is complete, they have access to the instance’s this context. A late field’s initializer can safely reference other instance methods, properties, or this itself, which is strictly prohibited for standard field initializers during object creation.
class Example {
  int baseValue = 10;
  
  // Valid: 'late' allows access to 'this.baseValue'
  late int derivedValue = baseValue * 2; 
}
Tired of Poor Dart Skills? Fix That With Deep Grasping!Learn More