Skip to content
Blog

Sound Null Safety in Dart: Why It Still Defines the Language

Dart's sound null safety is a compile-time guarantee, not a runtime check. Learn how the type system separates nullable from non-nullable values, and why that soundness is the foundation everything else in Dart builds on.

Published on • October 21, 2026

AI Assistant

Ask a Dart developer what changed most about the language and, past the recent headline features like primary constructors, the answer is usually the same: null safety. It is not the newest feature, but it is the load-bearing one. Nearly everything Dart now offers — pattern matching, sealed classes, sound async semantics — assumes a type system that knows, at compile time, whether a value can be null.

Source: Dart overview and the Dart language documentation, The Dart Team.

The distinction that does the work

The core idea is that Dart’s type system now splits every type into two: the non-nullable form and the nullable form. String can never be null. String? can be a String or null. That single ? is the whole mechanism, and the compiler enforces a rule that follows directly from it: you may only dereference a value after the compiler can prove it is non-null.

String? maybeName = getName();

// Error: the compiler cannot prove maybeName is non-null.
print(maybeName.length);

if (maybeName != null) {
  // Here the compiler *can* prove it — promotion kicks in.
  print(maybeName.length);
}

That if check triggers type promotion: within the branch, maybeName is promoted from String? to String, and dereferencing is allowed. Promotion is the ergonomic glue that makes the feature usable — without it, every check would be followed by a manual cast.

It is sound, which is the point

The word “sound” in “sound null safety” is doing real work. A sound guarantee means the compiler proves no null dereference can occur at runtime — not that it is unlikely, not that it is caught by testing. Null references, the so-called “billion-dollar mistake,” are eliminated at compile time for non-nullable types.

The trade is that the guarantee is only as strong as the boundary where untyped data enters your program. JSON from a network response, a native platform channel, or an FFI call can still hand you null from outside the type system. The discipline is to check once at that boundary and convert to a non-nullable type inside, so the rest of your code can rely on the compile-time guarantee.

Migration without a big bang

One reason Dart’s null safety landed so broadly is that it was rolled out by language version, not by a flag day. Each package opts in through its SDK constraint in pubspec.yaml, and a package that has not opted in still runs under the old, legacy semantics. That is what allowed a mixed-mode world — a legacy dependency alongside a null-safe one — during the ecosystem-wide migration, and why the analyzer treats pre-null-safe code differently rather than rejecting it outright.

For a package that has opted in, the compiler does the heavy lifting: it flags every place a nullable value is dereferenced without a check, and you fix them one at a time. dart fix automates many of the mechanical cases.

Why it underpins everything newer

The features that define modern Dart all lean on null safety being in place. Pattern matching’s null-aware patterns, late variables with deferred initialization, non-nullable fields in primary constructors, and the guarantees around Future and Stream types all assume the compiler already knows which types can be null. Remove the sound guarantee and those features lose their foundation.

That is why it remains the right mental model for the language even as release notes move on: when you are reasoning about a Dart type, the first question is still whether it is nullable, because the answer determines what the compiler will let you do with it.

Key takeaways

Dart’s sound null safety splits every type into non-nullable (String) and nullable (String?) forms, and the compiler enforces that you only dereference values it can prove are non-null — with type promotion handling the common if (x != null) case ergonomically. The guarantee is sound: null dereferences are proven impossible at compile time, with the discipline concentrated at boundaries where untyped data enters. Because it rolled out by language version rather than all at once, it became the foundation that pattern matching, sealed classes, and modern async APIs are all built on.

Further reading