In Dart, why do expressions like -1.abs(), await fetchUsers().length and cond ? a : b..add(x) not parse the way they read?
answer
- postfix binds before prefix
- await applies after the dot
- cascade sits below ?:
- shift is looser than +
- parentheses, locals, the formatter
basics
~20 sIn Dart, member access binds tighter than prefix operators, so -1.abs() is -(1.abs()) and await f().length awaits the length; a cascade binds looser than ?:, so b..add(x) after a conditional mutates whichever branch was chosen.
solid answer
~40 sDart's table puts postfix member access above every prefix operator, so `-1.abs()` means `-(1.abs())` and yields `-1`, and `await fetchUsers().length` asks the `Future` for a `length` it does not have; you need `(-1).abs()` and `(await fetchUsers()).length`. Cascades bind looser than `?:`, so `useCache ? cached : <String>[]..add('x')` cascades on the conditional's result and mutates `cached` when the flag is true. Other traps: `+` binds tighter than `<<`, so `1 << 2 + 1` is `8`, and `==` cannot be chained. I guard with parentheses around mixed operator families and cascades, locals for awaited values, `dart format` indentation in review, and tests with edge values.
code
dart · 18 linesFuture<List<String>> fetchUsers() async => ['ada', 'linus'];
Future<void> main() async {
print(-1.abs()); // -1: parsed as -(1.abs())
print((-1).abs()); // 1
// final n = await fetchUsers().length; // error: Future has no length
final n = (await fetchUsers()).length; // 2
print(n);
print(1 << 2 + 1); // 8: + binds tighter than <<
final cachedTags = <String>['a'];
const useCache = true;
final tags = useCache ? cachedTags : <String>[]..add('new');
print(cachedTags); // [a, new]: the cascade hit the cached list
print(identical(tags, cachedTags)); // true
}go deeper
Recall that parentheses decide what applies to what, and that -1.abs() and await x.length are the classic surprises.
Explain the order: postfix before prefix, arithmetic before shift before bitwise before comparison, and the cascade just above assignment.
Show the production bug: a cascade after ?: mutating shared cached state, and the review habits, parentheses, locals, formatter indentation and edge-value tests, that catch it.
Treat precedence as a code-review policy question: which lints and formatting rules the team relies on, and where the table is only an approximation of the grammar.
## Why precedence questions exist Dart's operator table orders operators from tightest to loosest binding. Most code never notices it, but a handful of expressions read one way and parse another. The dart.dev table is explicitly an **approximation**; the language grammar is authoritative. Interviewers ask about the cases below because each one has caused a real bug. ## The order, compressed | Tighter | Group | Operators | |---|---|---| | 1 | unary postfix and member access | `expr++` `expr--` `()` `[]` `.` `?.` `!` | | 2 | unary prefix | `-expr` `!expr` `~expr` `++expr` `--expr` `await` | | 3 | multiplicative | `*` `/` `%` `~/` | | 4 | additive | `+` `-` | | 5 | shift | `<<` `>>` `>>>` | | 6 | bitwise | `&`, then `^`, then pipe (OR) | | 7 | relational and type test | `<` `>` `<=` `>=` `as` `is` `is!` | | 8 | equality | `==` `!=` | | 9 | logical | `&&`, then `\|\|` | | 10 | if-null, conditional | `??`, then `?:` (right-associative) | | 11 | cascade | `..` `?..` | | 12 | assignment | `=` `+=` `~/=` and the rest (right-associative) | ## Trap 1: member access beats prefix operators Postfix member access binds tighter than any prefix operator, including unary minus and `await`. - `-1.abs()` is `-(1.abs())`, which is `-1`. Write `(-1).abs()` to get `1`. The SDK's own doc comments write `(-1).bitLength`, with the parentheses. - `await fetchUsers().length` is `await (fetchUsers().length)`. A `Future<List<User>>` has no `length`, so it does not compile. Write `(await fetchUsers()).length`. - `!await isReady()` is fine: `!` and `await` are both prefix, applied right to left. ## Trap 2: the cascade takes everything to its left Cascades sit **below** `?:` and just above assignment. The cascade therefore attaches to the complete conditional expression: ```dart final tags = useCache ? cachedTags : <String>[]..add('new'); ``` When `useCache` is true, `'new'` is added to `cachedTags`, silently mutating shared state, and `tags` is `cachedTags`. The fix is to parenthesise what you meant: `useCache ? cachedTags : (<String>[]..add('new'))`. The `unnecessary_parenthesis` lint explicitly treats parentheses that clarify a cascade's target as necessary. ## Trap 3: shifts and bitwise operators - **Additive binds tighter than shift**: `1 << 2 + 1` is `1 << 3`, which is `8`, not `5`. - **Bitwise binds tighter than equality in Dart**: `flags & mask == 0` parses as `(flags & mask) == 0`, which is usually what the author meant. Programmers used to the C-family table, where `==` binds tighter than `&`, often add parentheses defensively; in Dart they are for readability, not correctness. - **Relational and equality operators do not associate**: `a == b == c` and `a < b < c` are compile errors rather than surprising results. ## Trap 4: the conditional operator nests to the right `?:` is right-associative, so `a ? b : c ? d : e` means `a ? b : (c ? d : e)`. That is readable for a flat ladder of conditions but becomes opaque when a branch is itself a conditional in the middle position. In Dart 3 a switch expression is usually clearer for more than two outcomes. ## Guarding against precedence bugs 1. **Parenthesise mixed families**: any expression mixing shifts, bitwise and arithmetic, or putting a cascade next to `?:`, gets parentheses even when they are redundant. 2. **Pull a subexpression into a local**: `final users = await fetchUsers();` removes the `await` question entirely. 3. **Let `dart format` show the structure**: the formatter indents cascade sections under their receiver, so a cascade that attaches to a whole conditional is visible at a glance in review. 4. **Test the edge value**: a unit test with a negative number or a `true` flag catches the `-1.abs()` and cascade traps quickly. 5. **Read the grammar when it matters**: the table is a guide; the specification decides the ambiguous cases. ## What an interviewer listens for - The postfix-over-prefix rule, shown with `-1.abs()` or `await x.y`. - The cascade binding below `?:`, with the shared-state mutation it causes. - Additive over shift, and Dart's bitwise-over-equality order. - A habit, not a memorised table: parentheses, locals, formatter, tests.
- How do you fix the cascade in final tags = useCache ? cachedTags : <String>[]..add('new'); so it only touches the new list?Parenthesise the branch the cascade belongs to: `useCache ? cachedTags : (<String>[]..add('new'))`. Now the cascade's receiver is the new list only, and `cachedTags` is never mutated. The `unnecessary_parenthesis` lint accepts parentheses that clarify a cascade's target, so the analyzer will not push you to remove them.
- Does flags & mask == 0 need parentheses in Dart?Not for correctness. Dart places the bitwise operators above relational and equality operators, so it parses as `(flags & mask) == 0`. Many teams still write the parentheses because readers who learned the C-family order expect the opposite, and explicit grouping costs nothing.
- Why is a == b == c a compile error in Dart rather than a surprising true or false?Equality and relational operators have no associativity in Dart's grammar: an equality expression can hold at most one `==` or `!=`. So `a == b == c` and `a < b < c` are rejected at compile time instead of comparing a `bool` with `c`. Write `a == b && b == c` for the intended meaning.
saying these in an interview costs you the question
- Expects -1.abs() to print 1
- Thinks await binds tighter than member access
- Assumes a trailing cascade applies only to the else branch
- Believes 1 << 2 + 1 evaluates to 5
- Says a == b == c compiles and compares left to right