Operators and Expressions

Short-Circuit Evaluation

Short-circuit evaluation (also called minimal evaluation or lazy boolean evaluation) is the rule by which the binary logical operators && (AND) and || (OR) evaluate their right-hand operand only when the left-hand operand is not sufficient to determine the final result.

How && and || Short-Circuit

In left && right and left || right, Rust always evaluates left first:

  • For left && right:
    • If left is false, the entire AND expression must be false. Rust skips evaluating right and immediately returns false.
    • If left is true, Rust evaluates right and returns its result.
  • For left || right:
    • If left is true, the entire OR expression must be true. Rust skips evaluating right and immediately returns true.
    • If left is false, Rust evaluates right and returns its result.
Short-Circuit Flow for `left && right`:
  +---------------+     false
  | Evaluate left +------------------------> Returns `false` (`right` NEVER runs!)
  +-------+-------+
          | true
          v
  +----------------+
  | Evaluate right +-----------------------> Returns value of `right`
  +----------------+

Short-Circuit Flow for `left || right`:
  +---------------+     true
  | Evaluate left +------------------------> Returns `true` (`right` NEVER runs!)
  +-------+-------+
          | false
          v
  +----------------+
  | Evaluate right +-----------------------> Returns value of `right`
  +----------------+
fn check(label: &str, value: bool) -> bool {
    println!("  [check] {label} -> {value}");
    value
}

fn main() {
    println!("1. false && true:");
    let r1 = check("left", false) && check("right", true);
    println!("   Result = {r1}\n");

    println!("2. true && false:");
    let r2 = check("left", true) && check("right", false);
    println!("   Result = {r2}\n");

    println!("3. true || false:");
    let r3 = check("left", true) || check("right", false);
    println!("   Result = {r3}\n");

    println!("4. false || true:");
    let r4 = check("left", false) || check("right", true);
    println!("   Result = {r4}");
}
1. false && true:
  [check] left -> false
   Result = false

2. true && false:
  [check] left -> true
  [check] right -> false
   Result = false

3. true || false:
  [check] left -> true
   Result = true

4. false || true:
  [check] left -> false
  [check] right -> true
   Result = true

Guarding Potentially Panicking Operations

The most important practical use of short-circuit evaluation is writing inline guards where the left-hand condition verifies that the right-hand operation is safe to execute (preventing division by zero or out-of-bounds array indexing):

Unsafe Operation Guarded Short-Circuit Idiom Why It Is Safe
Division total / count count != 0 && total / count > 10 If count == 0, left is false and total / count is never evaluated
Slice index slice[i] i < slice.len() && slice[i] == target If i >= slice.len(), left is false and slice[i] is never indexed
Empty slice check slice.is_empty() || slice[0] == 0 If slice.is_empty() is true, left is true and slice[0] is never indexed
fn is_average_above(total: i32, count: i32, threshold: i32) -> bool {
    // Safe: when `count == 0`, the right-hand division `total / count` is skipped!
    count > 0 && (total / count) > threshold
}

fn starts_with_magic_byte(data: &[u8]) -> bool {
    // Safe: when `data` is empty, `data.len() >= 1` is `false` and `data[0]` is skipped!
    data.len() >= 1 && data[0] == 0x7F
}

fn main() {
    println!("avg(100, 0, 10):  {}", is_average_above(100, 0, 10));
    println!("avg(100, 4, 10):  {}", is_average_above(100, 4, 10));
    println!("magic(&[]):       {}", starts_with_magic_byte(&[]));
    println!("magic(&[0x7F]):   {}", starts_with_magic_byte(&[0x7F]));
}
avg(100, 0, 10):  false
avg(100, 4, 10):  true
magic(&[]):       false
magic(&[0x7F]):   true

Warning: Order matters! Writing data[0] == 0x7F && !data.is_empty() in the reverse order evaluates data[0] first and panics with an out-of-bounds error when data is empty. Always place the bounds or divisor check on the left side of && or ||.

Ordering Conditions for Performance

When combining a cheap in-memory check with an expensive check (such as a regex match, cryptographic hash verification, or database lookup), always place the cheapest and most selective check on the left:

fn cheap_prefix_check(s: &str) -> bool {
    println!("  Running fast O(1) prefix check...");
    s.starts_with("rust_")
}

fn expensive_deep_validation(s: &str) -> bool {
    println!("  Running expensive validation on '{s}'...");
    s.chars().all(|c| c.is_ascii_lowercase() || c == '_')
}

fn main() {
    let invalid_prefix = "python_script";
    let valid_candidate = "rust_compiler";

    println!("Checking '{invalid_prefix}':");
    let ok1 = cheap_prefix_check(invalid_prefix) && expensive_deep_validation(invalid_prefix);
    println!("  Result: {ok1}\n");

    println!("Checking '{valid_candidate}':");
    let ok2 = cheap_prefix_check(valid_candidate) && expensive_deep_validation(valid_candidate);
    println!("  Result: {ok2}");
}
Checking 'python_script':
  Running fast O(1) prefix check...
  Result: false

Checking 'rust_compiler':
  Running fast O(1) prefix check...
  Running expensive validation on 'rust_compiler'...
  Result: true

Side Effects in the Right-Hand Operand

Because the right-hand operand of && or || is conditionally skipped, avoid placing state-mutating expressions (such as incrementing a counter or writing to a log that must always happen) inside the right-hand operand of && or ||.

If both boolean expressions have side effects that must always run regardless of the left-hand outcome, either:

  1. Bind both results to local let variables before combining them with && or ||, or
  2. Use the non-short-circuiting (eager) boolean operators & and |.
Operator Type Evaluates Right Operand When Left Decided Result?
a && b Short-circuiting AND No (skips b if a == false)
a || b Short-circuiting OR No (skips b if a == true)
a & b Eager (non-short-circuiting) AND Yes (always evaluates both a and b)
a | b Eager (non-short-circuiting) OR Yes (always evaluates both a and b)

Diverging Expressions on the Right-Hand Side

Because the right-hand side of || only executes when the left-hand condition is false (and the right-hand side of && only executes when the left-hand condition is true), Rust allows diverging expressions of the never type ! (such as panic!, return, break, or continue) on the right-hand side of && and ||, though explicit if statements are more idiomatic for control flow.