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

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 = 36

Mini-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)

Pro Tips

📋 Quick Reference — Operators

GroupOperators
Arithmetic+ - * / %
Compound+= -= *= /= %=
Comparison== != < <= > >=
Logical&& || !
Bitwise& | ^ ! << >>

🎉 Lesson Complete!

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

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.