6.5Logical operators

Last updated June 12, 2026

Lesson 4.6 introduced ! and promised that its colleagues AND and OR would arrive in lesson 6.5. Its quiz even leaked the syntax (n >= 0 && n <= 100) with a note that this lesson would formalize it. Formalizing now.

Comparison operators ask one question. Real conditions usually have two or more parts: is the number in range and even? Is the answer yes or did they just hit enter? The logical operators combine bools into bigger bools:

OperatorFormtrue when...
logical NOT!xx is false
logical ANDx && yx and y are both true
logical ORx || yx is true, or y is, or both

Logical AND

&& answers "are both of these true?":

xyx && y
truetruetrue
truefalsefalse
falsetruefalse
falsefalsefalse

The range check is its natural habitat, and here lesson 4.6's promised payoff finally compiles:

fn main() {
    let n = 42;

    if n >= 0 && n <= 100 {
        println!("{n} is in range");
    }
}
42 is in range

Remember from lesson 6.1: this is the way to ask "between", because comparison chaining (0 <= n <= 100) is a compile error. Both sides of && must be complete, self-sufficient comparisons.

Logical OR

|| answers "is at least one of these true?":

xyx || y
truetruetrue
truefalsetrue
falsetruetrue
falsefalsefalse
fn main() {
    let answer = 'Y';

    if answer == 'y' || answer == 'Y' {
        println!("confirmed");
    }
}
confirmed

Note the shape: the variable is compared twice, once per alternative. New programmers often try to factor out the repetition:

fn main() {
    let answer = 'Y';

    if answer == 'y' || 'Y' {
        println!("confirmed");
    }
}
error[E0308]: mismatched types
 --> src/main.rs:4:25
  |
4 |     if answer == 'y' || 'Y' {
  |        -------------    ^^^ expected `bool`, found `char`
  |        |
  |        expected because this is `bool`

In English, "if the answer is y or Y" makes sense. To ||, each side must independently be a bool, and a bare 'Y' isn't one. In C, where characters happily convert to true-ish numbers, this exact mistake compiles and is always true, a bug with a decades-long body count. Rust's no-truthiness rule (lesson 4.6) turns it into a 30-second fix.

Short-circuit evaluation

&& and || have a property no other operators share: they may not evaluate their right side at all. If the left side of && is false, the answer is already false, so the right side is skipped. If the left side of || is true, the answer is already true; skipped likewise. This is called short-circuit evaluation, and it isn't just an optimization, it's a tool. It lets the left side stand guard for the right:

use std::io;

fn main() {
    println!("How many people are splitting the bill?");

    let mut count = String::new();
    io::stdin()
        .read_line(&mut count)
        .expect("failed to read input");
    let count: i32 = count.trim().parse().expect("that wasn't a whole number");

    if count != 0 && 100 / count >= 25 {
        println!("That's at least 25 each.");
    } else {
        println!("No big shares here.");
    }
}
How many people are splitting the bill?
0
No big shares here.

The right side divides by count, which lesson 6.2 taught you is a panic waiting for a zero. It never panics here: when count is 0, the left side is false and the division is never evaluated. Order matters; written the other way around, the guard guards nothing.

Key insight

The left operand always evaluates first, guaranteed (it's the same left-to-right promise from lesson 6.1, doing real work). The corollary: don't put anything on the right side of && or || that must happen, because some runs will skip it.

Precedence, mixing, and a NOT trap

In lesson 6.1's table, ! ranks near the top, && and || near the bottom, with && above ||. Two consequences.

First, mixing && and || without parentheses is legal but treacherous: a || b && c is a || (b && c), not (a || b) && c, because AND binds tighter. Even when the bare version is right, the next reader has to stop and check it.

Best practice

When && and || appear in the same expression, parenthesize. (a || b) && c costs four characters and zero misreadings.

Second, because ! binds so tightly, it grabs only the thing immediately next to it. To negate a comparison, parenthesize the comparison:

fn main() {
    let x = 5;
    let y = 7;

    println!("{}", !(x > y));
}
true

Write !x > y instead and ! lands on x alone before the comparison happens. On an integer x, ! isn't even an error: it means bitwise NOT (lesson 6.6's business), so !5 > 7 computes -6 > 7 and answers a question nobody asked. The parentheses aren't decoration.

De Morgan's laws

Speaking of negating compound conditions: there's a famous pair of identities, and a famous mistake. The mistake is "distributing" the !:

!(a && b)   is NOT the same as   !a && !b

The truth: pushing a NOT through a compound condition flips the connector. These are De Morgan's laws:

!(a && b)  ==  !a || !b     "not both"  =  "at least one is false"
!(a || b)  ==  !a && !b     "not either" = "both are false"

Sanity-check the first in English: "it is not true that the file exists AND is readable" means "either it doesn't exist, OR it isn't readable". The AND became an OR on the way through. When the operands are comparisons, remember each comparison also flips to its opposite: !(x >= 0 && x <= 100) becomes x < 0 || x > 100.

For advanced readers

You can prove both laws by brute force: build the truth table for each side and watch the columns match for all four combinations of a and b. It's five minutes well spent exactly once in your life.

While we're in the back room: a != b on two bools is exclusive OR ("exactly one is true"), there's a ^ operator that does the same, and & and | work on bools as non-short-circuiting AND and OR. You'll want each of those someday, none of them this chapter.

Quiz time

Question #1

Evaluate each by hand:

a) true && false b) true || false && false c) (1 > 2) || (2 > 1) d) !(3 != 3) e) (5 > 3) && (5 < 3)

Show solution

a) false (both must be true) b) true: AND binds tighter, so this is true || (false && false), and || short-circuits on a true left side without even looking c) true (false || true) d) true (3 != 3 is false; NOT flips it) e) false (nothing is both greater and less)

Question #2

Rewrite without the outer !, using De Morgan's laws:

if !(age >= 13 && age <= 19) {
    println!("not a teenager");
}
Show solution
if age < 13 || age > 19 {
    println!("not a teenager");
}

The && flips to ||, and each comparison flips to its opposite (>= to <, <= to >).

Question #3

What does this print? (Careful: count how many times check actually runs.)

fn check(n: i32) -> bool {
    println!("checking {n}");
    n > 0
}

fn main() {
    let result = check(-1) && check(2);
    println!("{result}");
}
Show solution
checking -1
false

check(-1) prints its line and returns false. With a false left side, && short-circuits: check(2) never runs, so "checking 2" never prints, and the result is false. If both lines appeared in your prediction, reread the short-circuit section; this exact skip is the bug and the feature.

That's the last of the everyday operators. The next lesson is the optional one: the operators that work a number one bit at a time. Skip ahead to the chapter summary if bits can wait.