Precedence and Associativity
How Rust determines the order of operations in expressions using precedence and associativity rules, with tables, examples, and common mistakes.
When an expression mixes several operators, Rust needs a consistent way to decide which parts belong together. Without rules, 1 + 2 * 3 could mean (1 + 2) * 3 or 1 + (2 * 3). Rust uses two layered mechanisms to resolve this: precedence (which operator gets to bind its operands first) and associativity (the direction of grouping when two operators have equal precedence). Together they turn a flat string of tokens into an unambiguous tree.
What Operator Precedence Does
Precedence answers the question: in a ⊙ b ⊚ c, does ⊙ grab b before ⊚ does? Rust gives every operator a numeric-like rank. Multiplication (*) ranks higher than addition (+), so in 1 + 2 * 3 the multiplication happens first. The parser sees it the same way you would after learning arithmetic: 1 + (2 * 3), evaluating to 7.
fn main() {
let result = 1 + 2 * 3;
println!("{}", result); // prints 7
}
2 * 3 is computed, then 1 + 6 is computed. The code reflects the grouping forced by precedence — the multiplication is more "sticky" than the addition. If you come from mathematics, Rust’s basic arithmetic precedence matches what you already expect.
Same as everyday arithmetic:
For the four common arithmetic operators, Rust respects the same precedence rules you learned in school: multiplication and division before addition and subtraction. This makes mental parsing of a + b * c feel natural.
What Associativity Controls
When two operators with the same precedence sit next to each other, precedence alone cannot decide the grouping. Should 10 - 5 - 2 be read as (10 - 5) - 2 or as 10 - (5 - 2)? Associativity picks a direction.
Most Rust binary operators are left-associative, meaning the leftmost operation is performed first. Subtraction falls into this category:
fn main() {
let x = 10 - 5 - 2;
println!("{}", x); // prints 3
}
The compiler groups it as (10 - 5) - 2 → 5 - 2 → 3. If subtraction were right-associative, the result would be 10 - (5 - 2) = 7. Rust’s choice mirrors how you would normally evaluate left to right.
A handful of operators are right-associative (they group from the right), and some are deliberately non-associative — Rust refuses to guess and forces you to write parentheses. The table in the next section marks each one.
Associativity is not evaluation order:
Associativity decides the structure of the expression tree, but it doesn’t guarantee that the left operand is executed before the right one. Precedence and associativity are purely about grouping. Execution order of sub‑expressions is a separate concern, though Rust usually evaluates binary operators left‑to‑right after grouping.
Rust’s Operator Precedence Table
Below is the full precedence order, from highest (tightest binding) to lowest. The “Associativity” column tells you how Rust groups consecutive operators from the same row.
| Precedence level | Operators | What they do | Associativity |
|---|---|---|---|
| Highest | Paths (ident, ::), method calls (.), function calls (()), indexing ([]), tuple access (.0) | Access expressions | Left‑to‑right (chaining) |
Unary -, !, *, &, &mut | Negation, logical NOT, dereference, borrow | Right‑to‑left | |
as | Type cast | Non‑associative | |
*, /, % | Multiplication, division, remainder | Left‑to‑right | |
+, - | Addition, subtraction | Left‑to‑right | |
<<, >> | Left shift, right shift | Left‑to‑right | |
& | Bitwise AND | Left‑to‑right | |
^ | Bitwise XOR | Left‑to‑right | |
| | Bitwise OR | Left‑to‑right | |
==, !=, <, >, <=, >= | Comparisons | Non‑associative | |
&& | Logical AND | Left‑to‑right | |
|| | Logical OR | Left‑to‑right | |
.., ..= | Range creation | Non‑associative | |
|args| body | Closures | (see note) | |
| Lowest | =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= | Assignment | Right‑to‑left |
Closure precedence sits between range and assignment:
Closure expressions bind tighter than assignment but looser than everything else, including range. That means let f = || a + b; is fine (the closure body captures a + b), and let f = || a .. b; also works as (|| (a .. b)). Assignment’s right‑associativity does not affect closures directly.
A few expression forms — return, break, continue — sit even lower, but they rarely appear nested inside other operators without a block. When in doubt, parentheses always win.
Left‑to‑Right Associativity in Practice
The majority of binary operators are left‑associative. This aligns with how you read code: from left to right. Consider division and multiplication together:
fn main() {
let value = 100 / 10 * 2;
println!("{}", value); // prints 20
}
100 / 10 is computed first because / and * share the same precedence and are left‑associative. The expression becomes (100 / 10) * 2, which is 10 * 2 = 20. Without associativity, the parser would not know whether to compute 100 / (10 * 2) = 5 instead.
Shift and bitwise operators are also left‑associative:
a << b >> c is parsed as (a << b) >> c, and x & y ^ z becomes (x & y) ^ z. The left‑to‑right rule is uniform, so you can rely on it without memorising special cases for each symbol.
Right‑to‑Left Associativity — Unary Operators and Assignment
Right‑associativity flips the grouping so the rightmost operator grabs its operand first. Two categories rely on this.
Unary operators (-, !, *, &, &mut) chain from the right. A double negation like - -5 is read as - (-5), which becomes 5. Rust won’t read it as (- -) 5 because that isn’t a valid construct. Right‑associativity makes - -x natural — the innermost unary wraps the tightest.
fn main() {
let x = - -5;
println!("{}", x); // prints 5
}
Assignment operators are right‑associative so that chained assignments work as expected. If you write x = y = 5, Rust first evaluates y = 5, then assigns that result (the unit value ()) to x? Actually the assignment expression returns the value assigned, so y = 5 yields 5. With right‑associativity, x = y = 5 becomes x = (y = 5). Both x and y end up as 5.
fn main() {
let mut x = 0;
let mut y = 0;
x = y = 5;
println!("x = {}, y = {}", x, y); // prints x = 5, y = 5
}
Without right‑associativity the parser would try (x = y) = 5, which makes no sense. This grouping is the same one that lets x += y += 1 compile (though that might be hard to read and best avoided).
Non‑Associative Operators — Where Rust Refuses to Guess
Some operators are marked non‑associative because chaining them without parentheses is either a mistake or genuinely ambiguous. Rust rejects these chains at compile time.
Comparison operators (==, !=, <, >, <=, >=) are the most important example. In mathematics, a < b < c means “b is between a and c”. In Rust it is a compile error:
fn main() {
let a = 1;
let b = 2;
let c = 3;
// let valid = a < b < c; // error: comparison operators cannot be chained
}
You must write a < b && b < c instead. This design prevents a class of bugs that plague C-like languages, where a < b < c silently evaluates as (a < b) < c and almost never does what the author intended.
The same rule applies to the type‑cast operator as. 1 as u32 as u64 is ambiguous — should it cast to u32 then to u64, or try to cast directly? Rust forces you to write (1 as u32) as u64 or choose one cast.
Range operators .. and ..= are also non‑associative. 1..3..5 could mean (1..3) .. 5 or 1 .. (3..5), neither of which makes practical sense. Rust requires parentheses to clarify intent. This interacts with precedence in a subtle way that trips up newcomers (see the pitfalls section below).
Compile errors for non‑associative chains:
Omitting parentheses around chained comparisons, casts, or ranges will produce a clear compiler error, not silent misbehaviour. The message points exactly at the ambiguous operator and suggests adding parentheses.
How Closures Sit in the Precedence Hierarchy
Closures have a unique position: they bind weaker than all binary operators but stronger than assignment. This lets you write things like:
fn main() {
let add_one = |x| x + 1;
let f = || a + b; // closure body is a + b, not (|| a) + b
}
Because + binds tighter than ||, the body captures a + b as a whole. If closures had lower precedence than assignment, let f = || a + b; would parse as let f = (|| a) + b; which wouldn’t compile. The current placement makes intuitive single‑expression closures seamless.
Using Parentheses to Override Everything
Parentheses have the highest effective priority — any expression wrapped in ( ... ) is treated as a single unit, regardless of the operators inside. This is the universal escape hatch.
fn main() {
let result = (1 + 2) * 3;
println!("{}", result); // prints 9, not 7
}
When the default grouping feels unclear, add parentheses. The compiler will not complain, and future readers (including your future self) will thank you. Rust’s official style guide even encourages parentheses around complex conditions even when they aren’t strictly required.
Common Mistakes That Arise from Precedence
A few patterns catch even experienced programmers off guard. Knowing them upfront saves debugging time.
Mixing bitwise & with ==
Bitwise AND & has higher precedence than ==. So x & mask == 0 is parsed as (x & mask) == 0, which is usually what you want when checking if specific bits are clear. However, x & mask == mask is (x & mask) == mask, which also tends to work correctly. The trap is when you incorrectly assume == binds tighter and then add extra parentheses “for safety” — you can make the expression wrong if you write x & (mask == 0).
Logical AND && has lower precedence than ==. cond && x == 5 groups as cond && (x == 5), which is standard. The real danger is swapping & and && mentally. Stick to clear parentheses when mixing bitwise and comparison.
fn main() {
let flags = 0b0101;
let mask = 0b0001;
// Bitwise & binds tighter than ==, so this works:
assert_eq!(flags & mask == mask, true);
// But logical && binds looser:
let cond = true;
assert_eq!(cond && flags == 0b0101, true); // groups as cond && (flags == 0b0101)
}
Bitwise vs logical operator precedence is easy to flip:
The relative tightness of & / ^ / | versus == is the reverse of many newcomers’ intuition because they mentally group all “logic‑like” operators together. When a condition mixes bitwise and comparison, write parentheses to make the grouping explicit — even if it’s technically correct without them.
Ranges and arithmetic
The range operator .. has lower precedence than arithmetic. That means 1..5 + 1 is parsed as (1..5) + 1, which tries to add a Range<i32> to an i32 and fails to compile. To get a range from 1 to 6, you must write 1..(5 + 1).
fn main() {
// let r = 1..5 + 1; // error: cannot add `Range<i32>` to `i32`
let r = 1..(5 + 1); // correct
println!("{:?}", r); // prints 1..6
}
Assignment in unusual places
Assignment has the lowest binary‑operator precedence, so x = 3 + 4 works as expected: 3 + 4 is computed first, then assigned. But chained assignment x = y = 5 is legal only if y is a mutable place expression. A common slip is trying to use it inside a let statement: let x = y = 5; is invalid syntax because let expects a pattern, not an assignment expression. Assignments belong in statement position after both variables have been declared.
fn main() {
let mut a = 0;
let mut b = 0;
a = b = 42; // OK: both become 42
// let c = d = 42; // error: `let` cannot destructure assignment
}
Precedence Is About Grouping, Not Execution Order
It’s tempting to think that higher‑precedence operations run first at runtime. That’s not guaranteed. Precedence defines the shape of the abstract syntax tree: a + b * c is grouped as a + (b * c). The compiler may still evaluate a before b * c or interleave them if no side effects forbid it. Rust specifies that binary operator operands are evaluated left‑to‑right, so a() + b() will call a first, then b. But that rule is independent of precedence; it would hold even if the operator were something else. When you need a strict ordering, separate the computation into sequential statements.
Summary
Rust’s expression parsing is predictable: a hierarchical precedence table, left‑associativity for most binary operators, right‑associativity for unary and assignment, and non‑associativity where ambiguity is likely. This set of rules gives you the same grouping you’d expect from school‑math for arithmetic, while closing the door on bugs like chained comparisons and mis‑grouped bitwise logic.
When reading code, trust the table. When writing code, when in doubt, add parentheses. They cost nothing and make intent immediately visible.