In Dart, what is null-shorting in a ?. chain, and how do ?[], ?.. and callback?.call() follow the same rule?
answer
- one null ends the whole chain
- each ?. marks a nullable link
- parentheses stop the shorting
- ?.. only on the first cascade
- no ?() call; use ?.call
basics
~20 sIn Dart, when the receiver of ?. is null, the entire rest of the chain is skipped and the expression is null, so a?.b.c needs no second ?. when b is non-nullable. ?[], ?.. and callback?.call() short-circuit the same way.
solid answer
~40 sNull-shorting means a null receiver in `?.` skips not just one access but **the rest of the chain**: in `profile?.settings.theme.toUpperCase()`, a null `profile` makes the whole expression `null` without touching `settings`, `theme` or the call. So you write `?.` only where a link really can be null; a redundant one draws an `invalid_null_aware_operator` warning, and each remaining `?.` documents a real source of null. The other null-aware forms join the chain: `overrides?['theme']` for subscripts, `buffer?..write('a')..write('b')` for cascades — only the first section needs `?..` — and `onChanged?.call(value)` for invoking a nullable function, since Dart has no `?()` syntax. Parentheses end a chain: `(profile?.settings).theme` is a compile-time error because the parenthesised part is nullable.
code
dart · 32 linesclass Notifications {
Notifications({this.email});
final bool? email;
}
class Settings {
Settings({this.notifications, this.overrides});
final Notifications? notifications;
final Map<String, String>? overrides;
}
class Profile {
Profile(this.settings);
final Settings settings; // non-nullable
}
void describe(Profile? profile, void Function(String)? onLog) {
final email = profile?.settings.notifications?.email ?? true;
final theme = profile?.settings.overrides?['theme'] ?? 'system';
onLog?.call('email=$email theme=$theme');
final buffer = profile == null ? null : StringBuffer();
buffer
?..write('email=$email')
..write(', theme=$theme');
print(buffer?.toString() ?? 'no profile');
}
void main() {
describe(Profile(Settings(overrides: {'theme': 'dark'})), print);
describe(null, null); // prints: no profile
}go deeper
Recall that one null receiver in ?. makes the whole chain null, and that optional callbacks are called with ?.call().
Explain where a chain ends — parentheses, operators, new variables — and how ?[] and ?.. join the same shorting rule.
Show you read each ?. as a statement about which link is nullable, and tighten types so chains carry only the ?. that are real.
Use the shape of null-aware chains in reviews as a signal of data-model quality: long chains of optional links usually mean the model needs a validated layer.
## What null-shorting means Before null safety, `a?.b.c` was a trap: if `a` was null, `a?.b` produced `null` and then `.c` was called on it and crashed. Dart's null safety adopted **null-shorting**: when the receiver of a null-aware operator is `null`, **the entire rest of the selector chain is skipped** and the whole expression evaluates to `null`. ```dart Profile? profile; profile?.settings.theme.toUpperCase(); // null; nothing after ?. runs ``` The chain includes member accesses, method calls **and their argument lists**, subscripts and assignments. So `profile?.settings.save(expensiveSnapshot())` does not evaluate `expensiveSnapshot()` when `profile` is null. ## One ?. per nullable link Because of shorting, a `?.` is needed only where a link in the chain can itself be null. With `Profile? profile`, a non-nullable `Settings settings` and a nullable `Notifications? notifications`: ```dart profile?.settings.notifications?.email ``` - the first `?.` handles a null `profile`; - `.settings` needs none — if we get there, `profile` was real and `settings` is non-nullable; - the second `?.` handles a null `notifications`. Adding a `?.` after `settings` draws the warning **`invalid_null_aware_operator`** ("the receiver can't be null because of short-circuiting"). The benefit is **documentation**: every `?.` in a chain marks a distinct place where `null` can enter. ## Where a chain ends A chain is a sequence of selectors on one expression. It ends at anything that is not a selector: - **Parentheses**: `(profile?.settings).theme` is a compile-time error, because the parenthesised expression has type `Settings?` and `.theme` is a plain access on it. - **Operators**: in `profile?.settings.fontScale + 1` the `+` is outside the chain, so it receives a nullable value and fails to compile; add a fallback first, `(profile?.settings.fontScale ?? 1.0) + 1`. - **A new statement or variable**: `final s = profile?.settings;` gives `s` the type `Settings?`, and later uses need their own handling. ## The rest of the family | Form | Instead of | Example | |---|---|---| | `?[]` | `[]` on a nullable receiver | `settings?.overrides?['theme']` | | `?..` | the first `..` of a cascade on a nullable receiver | `buffer?..write('a')..write('b')` | | `?.call(…)` | calling a nullable function | `onChanged?.call(value)` | **`?[]`** is conditional subscript access: `overrides?['theme']` is `null` when `overrides` is `null`, otherwise `overrides['theme']`. On a `Map`, the lookup itself can still return `null` for a missing key, so the result is nullable either way. **`?..`** starts a **null-shorting cascade**. Write it only on the first section: if the target is `null`, none of the following `..` sections run. `buffer?..write('a')..write('b')` is equivalent to two `buffer?.write` calls. **`?.call(…)`**: Dart has no null-aware call syntax such as `onTap?()`. Every function value has a `call` method, so `onTap?.call()` invokes it only when it is non-null — the standard way to fire an optional callback parameter such as `VoidCallback? onTap` in Flutter. ## Common mistakes - Sprinkling `?.` on every link "to be safe", which hides which values are truly optional. - Expecting a parenthesised null-aware expression to keep shorting. - Writing `onTap!()` to call an optional callback, which crashes when the caller passed none. - Using `?..` on every cascade section.
- In Dart, what is the static type of profile?.settings.theme when theme is a non-nullable String?`String?`. The chain can be short-circuited by a null `profile`, so the whole expression may be `null` even though no individual member is nullable after the first link. Add `?? 'system'` to end with a `String`.
- In Dart, if profile is null, does profile?.settings.save(buildSnapshot()) call buildSnapshot()?No. Null-shorting skips the rest of the chain, and the argument list of `save` is part of it, so `buildSnapshot()` is never evaluated. Side effects placed in arguments of a shorted call do not happen.
saying these in an interview costs you the question
- Every link after the first ?. needs its own ?. to be safe.
- (a?.b).c still short-circuits when a is null.
- Dart lets you call a nullable function with onTap?().
- Each section of a nullable cascade needs its own ?.. operator.
- Arguments of a shorted method call are still evaluated.