Null safety basics
Under sound null safety the type system splits in two: non-nullable T and nullable T?. A String name can never be null — the compiler guarantees it, so no check is needed at the call site. A String? name may be null and the compiler requires a check before you use it.
The word "sound" matters here: the guarantee holds both at compile time and in the runtime type information — which is why the compiler can remove null checks entirely and generate faster code. This differs from TypeScript's strictNullChecks, where the check exists only at compile time.
The practical consequence: a null error stops being a "runtime surprise" and becomes a design decision. If a field is T?, it says "this value can genuinely be absent" and you chose that deliberately.
| Operator | What it does | Example |
|---|---|---|
| `?.` | Null-aware access — if the object is `null`, the result is `null` | `user?.name` |
| `??` | Default value — returns the right side when the left is `null` | `name ?? 'Guest'` |
| `??=` | Assigns only when the current value is `null` | `cache ??= compute();` |
| `!` | Bang — asserts "this is not null"; throws at runtime if wrong | `user!.name` |
| `?..` | Null-aware cascade | `user?..save()..log()` |
| `?[]` | Null-aware index | `list?[0]` |
Flow analysis and promotion. The compiler tracks control flow: inside if (name != null) the variable name behaves as String rather than String? — this is type promotion. No ?. or ! is needed.
Promotion also works with early returns: after the line if (name == null) return;, name counts as promoted for all remaining code. That makes the guard-clause style particularly effective in Dart.
There are cases where promotion does not apply, and those are exactly what interviews ask about:
- Public fields — neither a non-
finalinstance field nor a getter is promoted, and a publicfinalfield is not promoted either, because a subclass could override it with a getter - Top-level and static variables
- Local variables mutated inside a closure
The reason: the compiler only promotes a value it can guarantee will not change in between. For a public field there is no such guarantee — another method could reassign it, and a subclass could override it with a getter that returns a different value on each call.
There is one exception: since Dart 3.2, private `final` fields (final String? _token;) are promoted, because the compiler sees the whole library and knows the field cannot be overridden. The practical consequence: build your models out of private final fields and the need for ! shrinks on its own.
When is `!` a design smell? The bang tells the compiler "I know better". Sometimes you genuinely do (for example a local value checked one line above), but a rising density of ! usually signals that the model is wrong.
Typical causes and their proper fixes:
- The field is
T?because it is not ready in the constructor → `late final`, or two separate state classes - The field is
T?because it is absent in some cases → separate those cases with a `sealed` hierarchy - You need a default when the value arrives as
null→ `??` - You read a field and then use it → copy it into a local (
final n = this.name; if (n != null) ...)
In practice teams pair the relevant lints with a review rule such as "a new ! requires a comment explaining why it cannot be null".
Interview tip. "Why is a nullable field not promoted while a local is?" is the single most asked null-safety question, and a surface answer ("that's how it works") is not enough. A strong answer explains the reason: "The compiler only promotes a value it can guarantee will not change in between. For a local it has that guarantee. A field can be reassigned from another method, a getter can return a different value on each call, and a subclass can override it — so two reads are unrelated." Then show two practical fixes: final local = field; if (local != null) { ... } gives you promotion without a single !; and making the field private and final, which since Dart 3.2 is promotable.
📚 Sources and documentation
- Sound null safetyofficialdart.dev
- The Dart type systemofficialdart.dev