Operators & Expressions
Reviewed & published by Brayan K
Operators are the symbols that combine values — arithmetic, comparison, logical, and bitwise — and in Rust nearly every construct is an expression that produces a value you can use directly.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll learn Rust's operators, how integer overflow is handled with wrapping_add and friends, why there's no ++, and how if and blocks return values.
What You'll Learn in This Lesson
- Arithmetic operators and integer vs float division
- Overflow behavior and the wrapping_/checked_/saturating_ methods
- Comparison operators that return a bool
- Logical && and || (with short-circuiting) and bitwise operators
- Why Rust has no ++ or -- (use += instead)
- That if and blocks are expressions that produce a value
1️⃣ Arithmetic & Overflow
The arithmetic operators + - * / % behave as you'd expect, with one catch: dividing two integers truncates toward zero, while dividing floats keeps the fraction. Remember there is no ++ — increment with += on a mut variable.
fn main() {
// Arithmetic operators work on numbers of the same type.
let a = 17;
let b = 5;
println!("a + b = {}", a + b); // addition
println!("a - b = {}", a - b); // subtraction
println!("a * b = {}", a * b); // multiplication
println!("a / b = {}", a / b); // INTEGER division truncates toward zero
println!("a % b = {}", a % b); // remainder (modulo)
// Float division keeps the fractional part.
let x = 17.0_f64;
let y = 5.0_f64;
println!("x / y = {}", x / y);
// Rust has NO ++ or -- operators. Use += instead.
let mut count = 0;
count += 1;
count += 1;
println!("count = {}", count);
}Integers have fixed sizes, so a result can be too big to fit. Rust does not silently corrupt data: a debug build panics on overflow. When you actually want a specific behavior, you ask for it by name:
fn main() {
// u8 holds 0..=255. Adding past the top would PANIC in a debug build,
// so for intentional wrap-around use the explicit wrapping_* methods.
let max: u8 = 255;
let wrapped = max.wrapping_add(1); // wraps around to 0
println!("255u8.wrapping_add(1) = {}", wrapped);
// checked_* returns None instead of overflowing.
let safe = max.checked_add(1);
println!("255u8.checked_add(1) = {:?}", safe);
println!("200u8.checked_add(10) = {:?}", 200u8.checked_add(10));
// saturating_* clamps to the type's min/max instead of wrapping.
println!("255u8.saturating_add(50) = {}", max.saturating_add(50));
}2️⃣ Comparison, Logical & Bitwise
Comparison operators (< <= > >= == !=) always produce a bool. The logical operators && and || combine booleans and short-circuit — they stop evaluating as soon as the answer is decided. The bitwise operators & | ^ ! << >> work on the individual bits of integers.
fn main() {
// Comparison operators return a bool.
println!("3 < 5 = {}", 3 < 5);
println!("3 == 5 = {}", 3 == 5);
println!("3 != 5 = {}", 3 != 5);
// Logical && and || SHORT-CIRCUIT (stop early when the answer is known).
let t = true;
let f = false;
println!("t && f = {}", t && f);
println!("t || f = {}", t || f);
println!("!t = {}", !t);
// Bitwise operators work on the binary bits of integers.
let a: u8 = 0b1100; // 12
let b: u8 = 0b1010; // 10
println!("a & b = {}", a & b); // AND -> 8
println!("a | b = {}", a | b); // OR -> 14
println!("a ^ b = {}", a ^ b); // XOR -> 6
println!("a << 1 = {}", a << 1); // shift left -> 24
}Don't confuse && (logical AND on booleans, short-circuiting) with & (bitwise AND on integer bits). They are different operators for different jobs.
3️⃣ Everything Is an Expression
This is what makes Rust feel different. An if/else is an expression: it evaluates to a value you can bind to a variable. A block { } evaluates to its last line when that line has no trailing semicolon. So does match. You rarely need a temporary mutable variable just to compute something conditionally.
fn main() {
// In Rust, `if` is an EXPRESSION — it evaluates to a value.
let score = 72;
let grade = if score >= 60 { "pass" } else { "fail" };
println!("grade = {}", grade);
// A block { } is also an expression: its last line (no semicolon) is its value.
let doubled = {
let n = 21;
n * 2 // no semicolon -> this is the block's value
};
println!("doubled = {}", doubled);
// Because everything is an expression, you can assign directly from one.
let label = match score / 10 {
10 | 9 => "A",
8 => "B",
7 => "C",
_ => "lower",
};
println!("label = {}", label);
}Your turn. Fill in the blanks marked ___, then run it.
// 🎯 YOUR TURN — fill in the blanks, then run it.
fn main() {
let n = 18;
// 1) Use the remainder operator to test even vs odd.
let parity = if n ___ 2 == 0 { "even" } else { "odd" };
println!("{} is {}", n, parity);
// 2) Rust has no ++ — increment with the compound operator.
let mut total = 0;
total ___ n;
total ___ n;
println!("total = {}", total);
}
// ✅ Expected output:
// 18 is even
// total = 36Mini-Challenge: Number Cruncher
Combine arithmetic, the remainder operator, and a comparison to summarize an array. Run it with cargo run and check the output.
// 🎯 MINI-CHALLENGE: number cruncher.
//
// Given the array [6, 7, 8, 9, 10]:
// 1. Sum every element with += in a for loop.
// 2. Count how many are even using % and an if.
// 3. Compute the integer average (sum / count).
// Hint: nums.len() is a usize — cast it with `as i32`.
//
// ✅ Example output:
// sum = 40
// evens = 3
// avg = 8
fn main() {
let nums = [6, 7, 8, 9, 10];
// your code here
}Common Errors (and the fix)
- ❌ x++ "expected expression" — Rust has no ++. ✅ Write x += 1; (and declare x as mut).
- ❌ "mismatched types: expected i32, found f64" — You mixed numeric types in one operation. ✅ Cast explicitly with as, e.g. (n as f64) / 2.0.
- ❌ "attempt to add with overflow" panic — A debug build caught an overflow. ✅ Use a bigger type, or state intent with wrapping_add / checked_add / saturating_add.
- ❌ if "returns" () — You put a ; after the value in a branch. ✅ Drop the semicolon so the branch is an expression.
Pro Tips
- 💡 Prefer if as an expression: let x = if cond { a } else { b }; beats a mutable temp variable.
- 💡 Name your overflow intent: reaching for checked_* in untrusted-input code prevents surprise panics in release.
- 💡 Use 0b, 0o, 0x literals when working with bitwise operators — they make the bit patterns obvious.
📋 Quick Reference — Operators
| Group | Operators |
|---|---|
| Arithmetic | + - * / % |
| Compound | += -= *= /= %= |
| Comparison | == != < <= > >= |
| Logical | && || ! |
| Bitwise | & | ^ ! << >> |
🎉 Lesson Complete!
- ✅ Integer division truncates; float division keeps the fraction
- ✅ Overflow panics in debug — use wrapping_/checked_/saturating_ to state intent
- ✅ &&/|| short-circuit; & | ^ << >> work on bits
- ✅ No ++; if, blocks, and match are expressions that produce values
Practice quiz
What is the result of 17 / 5 in Rust when both are integers?
- 3.4
- 3
- 4
- 2
Answer: 3. Integer division truncates toward zero, so 17 / 5 is 3.
What does the % operator compute?
- Percentage
- Remainder (modulo)
- Exponent
- Division
Answer: Remainder (modulo). % gives the remainder, e.g. 17 % 5 is 2.
How do you increment a mutable variable in Rust?
- count++
- ++count
- count += 1
- count =+ 1
Answer: count += 1. Rust has no ++; use the compound operator count += 1.
What does 255u8.wrapping_add(1) return?
- 256
- 0
- None
- a panic
Answer: 0. wrapping_add wraps a u8 around from 255 back to 0.
What does 255u8.checked_add(1) return?
- Some(0)
- 0
- None
- 256
Answer: None. checked_add returns None when the operation would overflow.
What does 255u8.saturating_add(50) return?
- 305
- 49
- 255
- None
Answer: 255. saturating_add clamps to the type maximum, which is 255 for u8.
What is 0b1100 & 0b1010 (bitwise AND)?
- 8
- 14
- 6
- 12
Answer: 8. Bitwise AND keeps only bits set in both, giving 0b1000 which is 8.
Which operators short-circuit on booleans?
- & and |
- && and ||
- ^ and !
- << and >>
Answer: && and ||. Logical && and || stop evaluating as soon as the answer is known.
What does let g = if x >= 60 { "pass" } else { "fail" }; show?
- if is a statement
- if is an expression that yields a value
- This is a syntax error
- g is always ()
Answer: if is an expression that yields a value. In Rust if is an expression, so it can be bound directly to a variable.
Why does Rust reject 5 + 2.0 directly?
- Floats are not allowed
- No implicit numeric coercion; operands must share a type
- + only works on integers
- 2.0 is not a number
Answer: No implicit numeric coercion; operands must share a type. Rust has no implicit numeric coercion; cast explicitly, e.g. (5 as f64) + 2.0.
Continue this course
- Previous: Data Types
- Next: Tuples & Arrays — Fixed-size groupings — destructuring tuples and indexing arrays safely
- Quick reference: Rust cheat sheet
Frequently asked questions
Why doesn't Rust have ++ or -- operators?
Rust deliberately omits the increment (++) and decrement (--) operators that C, Java, and JavaScript have. They were a frequent source of subtle bugs around evaluation order and prefix-versus-postfix confusion, and they conflict with Rust's emphasis on explicit, readable code. Instead you use the compound assignment operators: count += 1 to increment and count -= 1 to decrement. The variable also has to be declared mut to be reassigned at all, so mutation is always visible at the point of declaration.
What happens when integer arithmetic overflows in Rust?
It depends on the build. In a debug build, an overflowing operation like 255u8 + 1 panics immediately so you catch the bug, whereas in a release build it wraps around silently (two's-complement) by default. Because relying on either is risky, Rust gives you explicit methods to state your intent: wrapping_add wraps on purpose, checked_add returns an Option that's None on overflow, saturating_add clamps to the type's maximum or minimum, and overflowing_add returns the value plus a bool flag. Choosing one makes the behavior unambiguous in every build.
What does it mean that 'everything is an expression' in Rust?
Most constructs in Rust produce a value rather than just performing an action. An if/else evaluates to a value, so you can write let grade = if pass { "A" } else { "F" }. A block delimited by braces evaluates to its final expression when that line has no trailing semicolon. match and loop (with break value) are expressions too. This lets you initialize variables directly from control flow without temporary mutable variables, which keeps code concise and reduces a whole class of uninitialized-value bugs.
Why can't I add an i32 to an f64 directly?
Rust has no implicit numeric coercion: the two operands of an arithmetic operator must already be the same type. Mixing them, like 5 + 2.0, is a compile error. This prevents accidental precision loss and surprising conversions. You convert explicitly, for example (5 as f64) + 2.0, or annotate your literals so they share a type. It feels strict at first, but it makes the numeric behavior of every expression obvious at a glance.