In Dart, what do the ?., ?? and ??= operators do, and when is each one the right tool?
answer
- skip, substitute, fill in
- ?. yields null on a null receiver
- ?? evaluates its right side lazily
- ??= assigns only when null
- ?? sits below || in precedence
basics
~20 sIn Dart, a?.b reads b only when a is non-null and otherwise yields null; a ?? b yields a unless it is null, evaluating b only then; a ??= b assigns b only when a is currently null.
solid answer
~40 s`?.` is conditional member access: `profile?.settings` is `null` when `profile` is `null`, otherwise `profile.settings`, so its type is nullable. `??` is the if-null operator: `theme ?? 'system'` gives `theme` unless it is `null`, and only then evaluates the right operand; when that operand is non-nullable, the result is non-nullable. `??=` assigns only when the target is currently `null` — `_cache ??= load()` fills a cache once and leaves an existing value alone — and the expression evaluates to the variable's resulting value. They combine naturally: `profile?.settings?.theme ?? 'system'` walks a chain of optional values and ends in a real `String`. Watch precedence: `??` binds more loosely than arithmetic and `||`, so `count ?? 0 + 1` means `count ?? (0 + 1)`.
code
dart · 25 linesclass Settings {
Settings({this.theme, this.fontScale});
String? theme;
double? fontScale;
}
class Profile {
Profile({this.settings});
final Settings? settings;
}
Profile? loadProfile(bool signedIn) =>
signedIn ? Profile(settings: Settings(theme: 'dark')) : null;
void main() {
final profile = loadProfile(true);
final guest = loadProfile(false);
print(profile?.settings?.theme ?? 'system'); // dark
print(guest?.settings?.theme ?? 'system'); // system
profile?.settings?.fontScale ??= 1.0; // fills the missing value
profile?.settings?.fontScale ??= 2.0; // no effect: already 1.0
print(profile?.settings?.fontScale); // 1.0
}go deeper
Recall the three meanings: ?. skips on null, ?? substitutes a default, ??= fills a null variable, and that ?? ends a chain with a non-null value.
Explain the lazy right operand of ?? and ??=, the result types, and the precedence of ?? relative to arithmetic and the conditional operator.
Show you pick defaults deliberately, avoid double evaluation with ?? over ternaries, and parenthesise mixed expressions so reviewers never guess.
Decide where defaults belong in a codebase, at the data boundary or at each use, so the same missing value is not defaulted differently in two places.
## Three operators, three jobs The **null-aware operators** let you work with nullable values without first writing an `if`. Each does one job: | Operator | Name | Reads as | Result type | |---|---|---|---| | `a?.b` | conditional member access | "`a.b`, unless `a` is null" | nullable | | `a ?? b` | if-null | "`a`, or `b` if `a` is null" | non-nullable when `b` is | | `a ??= b` | null-aware assignment | "set `a` to `b` if `a` is null" | the variable's resulting value | ## ?. — skip the access `profile?.settings` evaluates `profile`; if it is `null`, the member access is skipped and the whole expression is `null`. If not, it is ordinary `profile.settings`. The type of the result is always nullable, because the skip path yields `null`. - It works for getters, setters and method calls: `profile?.save()` calls `save` only on a real profile. - Its companions `?[]` (subscript) and `?..` (cascade) follow the same rule, and a chain of them **short-circuits** as a unit. - Using `?.` on a receiver that cannot be null draws an analyzer warning (`invalid_null_aware_operator`), which is a hint that the type is stricter than you thought. ## ?? — substitute a default `a ?? b` evaluates `a`. If it is non-null, that is the result and **`b` is never evaluated**. Only when `a` is `null` does Dart evaluate `b`. This laziness matters when `b` is expensive or has side effects. The static type follows: with `String? theme`, `theme ?? 'system'` has type `String`. That is how a chain of optional reads ends in a value you can use directly: ```dart final theme = profile?.settings?.theme ?? 'system'; // String ``` If the left operand can never be null, the analyzer warns (`dead_null_aware_expression`: the right operand is never executed). ## ??= — fill in once `a ??= b` assigns `b` to `a` only when `a` is currently `null`; otherwise it leaves `a` unchanged and does not evaluate `b`. The expression's value is `a`'s value afterwards. Typical uses: 1. **Memoizing**: `_avatar ??= decodeAvatar(bytes);` computes on first use only. 2. **Defaults on optional parameters**: `limit ??= 20;` at the top of a function. 3. **Map entries**: `counts[key] ??= 0;` creates an entry only when the lookup returns `null`. The `unnecessary_null_aware_assignments` lint (recommended set) flags `x ??= null`, which can never do anything. ## Precedence traps In Dart's precedence table, `??` sits **below** arithmetic, comparison, `&&` and `||`, and **above** the conditional `?:` and assignment. So: - `count ?? 0 + 1` is `count ?? (0 + 1)` — write `(count ?? 0) + 1` for the other meaning. - `'Hi ' + name ?? 'guest'` parses as `('Hi ' + name) ?? 'guest'` and fails to compile, because `+` receives a `String?`. Use interpolation with the fallback inside: `'Hi ${name ?? 'guest'}'`. - `a ?? b ? x : y` means `(a ?? b) ? x : y`. Parentheses cost nothing and remove the doubt. ## Choosing between them - The value is optional and you only want to act if it exists → `?.`. - You need a real value and a sensible default exists → `??`. - A variable should hold a default or computed value from now on → `??=`. - No sensible default exists and absence is a bug → none of these; see the `!` operator and explicit throws.
- In Dart, what is the difference between a ?? b and a != null ? a : b?For a local variable they behave the same. For any other expression, `??` evaluates `a` once, while the conditional evaluates it twice — a getter or method call runs twice and may return different values. `??` is also shorter, and the `prefer_if_null_operators` lint in the recommended set suggests it.
- In Dart, what does the expression x ??= compute() evaluate to when x is already non-null?It evaluates to the current value of `x`, and `compute()` is never called. When `x` is `null`, `compute()` runs, its result is stored in `x`, and that value is the result of the whole expression.
saying these in an interview costs you the question
- The right operand of ?? is always evaluated, then discarded if unused.
- a?.b throws when a is null, just more politely than a.b.
- a ??= b overwrites a with b whenever b is non-null.
- count ?? 0 + 1 adds one to the count or to zero.
- ?? treats empty strings and zero as missing values too.