Skip to content

07 · Null Safety Basics

What "sound null safety" means

Since Dart 2.12, every type is non-nullable by default. A variable declared String name can never hold null — the compiler guarantees it, not just at your call sites but everywhere in the whole program. To allow null, you opt in explicitly with a ? suffix on the type.

void main() {
  String name = 'Ada';
  // name = null;   // compile error: null can't be assigned to non-nullable String

  String? maybeName = 'Ada';
  maybeName = null;   // fine -- String? explicitly allows null
  print(maybeName);   // null
}

This eliminates most NullPointerException-style crashes at compile time rather than runtime — if your code compiles, the compiler has already proven every non-nullable variable is never null when used.

The ? nullable type suffix

void main() {
  int? age;                 // defaults to null, since no initializer given
  print(age);                // null

  age = 30;
  print(age + 1);            // 31 -- Dart knows age is non-null here (type promotion)
}

The ! null-assertion operator

! tells the compiler "trust me, this is not null right now" — it throws at runtime if you're wrong, so use it sparingly and only when you're certain.

void main() {
  String? maybeText = fetchText();
  // print(maybeText.length);   // compile error -- maybeText might be null

  print(maybeText!.length);     // 5 -- asserts non-null, then accesses .length
}

String? fetchText() => 'hello';
void main() {
  String? risky;
  print(risky!.length);   // throws at runtime: "Null check operator used on a null value"
}

The ?? and ??= null-coalescing operators

void main() {
  String? nickname;
  String display = nickname ?? 'Anonymous';   // use right-hand side if left is null
  print(display);   // Anonymous

  nickname ??= 'Ada';    // assign only if nickname is currently null
  print(nickname);      // Ada

  nickname ??= 'Grace';  // no-op, nickname is already non-null
  print(nickname);      // Ada
}

The ?. null-aware access operator

class Address {
  String? city;
  Address(this.city);
}

class Person {
  String name;
  Address? address;
  Person(this.name, [this.address]);
}

void main() {
  var p1 = Person('Ada', Address('London'));
  var p2 = Person('Grace');   // no address

  print(p1.address?.city);   // London
  print(p2.address?.city);   // null -- short-circuits safely, no crash

  // Chain multiple ?. together
  print(p2.address?.city?.toUpperCase());   // null
}

Null-aware operators for collections

void main() {
  List<int>? numbers;

  print(numbers?.length);        // null -- safe access on a possibly-null list
  print(numbers?.isEmpty ?? true);   // true

  Map<String, int>? scores;
  print(scores?['Ada'] ?? 0);    // 0
}

Type promotion

Dart's analyzer tracks null checks and "promotes" a nullable variable to its non-nullable type within the scope where it's known to be safe.

void printLength(String? text) {
  if (text != null) {
    // Inside this block, `text` is promoted to String (non-nullable)
    print(text.length);
  } else {
    print('No text provided');
  }
}

void main() {
  printLength('hello');   // 5
  printLength(null);      // No text provided
}

Late variables

late promises the compiler a non-nullable variable will be initialized before it's first read — useful when initialization can't happen at declaration time (e.g. depends on constructor logic run afterward).

class Config {
  late String environment;   // will be set before use, trust me

  void load() {
    environment = 'production';
  }
}

void main() {
  var config = Config();
  config.load();
  print(config.environment);   // production

  // var badConfig = Config();
  // print(badConfig.environment);   // runtime error: LateInitializationError
}

Cheat sheet

Operator Meaning
String? Nullable type — may hold null
! Assert non-null now; throws at runtime if wrong
?? Fallback value if the left side is null
??= Assign only if currently null
?. Access a member only if the receiver isn't null
late Defer non-null initialization until before first use

How It Actually Works

Dart's null safety is sound, which is a specific, checkable guarantee: the compiler proves that a variable typed T (no ?) can never hold null at runtime, not just "probably won't." This is enforced at two points — statically by the analyzer/compiler rejecting code that could assign null to a non-nullable type, and at API boundaries (like FFI or platform channels that hand you supposedly-typed data from outside Dart's type system) by runtime null checks inserted by the compiler. Because the guarantee is sound rather than merely advisory, the AOT and JIT compilers can use non-nullability as an optimization signal: a field declared int (not int?) can be stored unboxed/without a null-check branch on every read, because the compiler has proven no null ever reaches it — this is a real performance win, not just a documentation aid.

The ! null-assertion operator compiles to an actual runtime check — it inserts a conditional that throws a TypeError if the value is null, then otherwise treats the value as the non-nullable type from that point on. It's the only null-safety construct that can still crash at runtime (by design — you're telling the compiler "trust me"), whereas ?., ??, and type promotion are all compiler-verified and can't fail at runtime for the case they check.

late variables use a different mechanism entirely: the compiler generates a hidden backing field plus a boolean "initialized" flag. A late field/local skips Dart's usual "definite assignment" compile-time check and instead defers that check to first access — reading it before it's set throws a LateInitializationError at runtime. For late fields with an initializer expression, the field is genuinely lazy: the initializer only runs the first time the field is read, exactly like a lazily-initialized static final, and subsequent reads return the cached value instead of re-running the initializer.

🔀 See this in another language

Exercise

Write a function String describeUser(String? name, int? age) that returns "Name: <name>, Age: <age>" using ?? to substitute "Unknown" for a null name and 0 for a null age. Then model a Team class with a nullable Person? captain field, and write a function that prints the captain's name in upper case using ?. chaining, falling back to "No captain assigned" if there is none.