Operators and Expressions

Block Expressions

A block expression is a sequence of zero or more statements and an optional final tail expression enclosed in curly braces { ... }. In Rust, any block is an expression that creates a new lexical scope, executes its statements in order, and evaluates to a value.

Anatomy of a Block Expression

let hypotenuse_sq: i32 = {
    let dx = 3;          <-- 1. Statement (`let` declaration, ends with `;`)
    let dy = 4;          <-- 2. Statement (`let` declaration, ends with `;`)
    println!("computing");<-- 3. Statement (expression statement, ends with `;`)
    dx * dx + dy * dy    <-- 4. Tail expression (no `;` -> becomes the block's value: `25`)
};
  • If a block ends with a tail expression (no trailing ;), the block's type and value are the type and value of that tail expression.
  • If a block is empty {} or ends with a semicolon-terminated statement, the block evaluates to the unit value () of type ().
fn main() {
    let area: u32 = {
        let width = 12;
        let height = 5;
        width * height // Tail expression -> block evaluates to `60`
    };

    let unit_result: () = {
        let temp = 42;
        println!("Side-effect block with temp = {temp}"); // Ends with `;` -> evaluates to `()`
    };

    println!("area = {area}, unit_result = {unit_result:?}");
}
Side-effect block with temp = 42
area = 60, unit_result = ()

Isolating Intermediate Variables with Initialization Blocks

A common Rust idiom uses a block expression on the right-hand side of let to compute a complex value using temporary helper variables without polluting the surrounding function's scope or keeping mut active longer than necessary:

fn main() {
    // `raw`, `trimmed`, and `mut buffer` exist ONLY inside the `{ ... }` initialization block!
    let formatted_tag: String = {
        let raw = "  rust-2024  ";
        let trimmed = raw.trim();
        let mut buffer = String::from("[");
        buffer.push_str(&trimmed.to_uppercase());
        buffer.push(']');
        buffer // Moved out into `formatted_tag` as an IMMUTABLE String!
    };

    // Here, `raw`, `trimmed`, and `buffer` are out of scope, and `formatted_tag` is immutable:
    println!("Result: {formatted_tag}");
}
Result: [RUST-2024]

Shortening Borrow Lifetimes (Scoping Locks and Mutable Borrows)

Because every variable declared inside a block is dropped at the block's closing brace }, standalone blocks { ... } are also used to explicitly end a mutable borrow (&mut T) or release a lock guard (MutexGuard) before the rest of the function continues:

fn main() {
    let mut data = vec![10, 20, 30];

    {
        // `slice_mut` borrows `data` mutably only inside this inner block:
        let slice_mut: &mut [i32] = &mut data[0..2];
        slice_mut[0] += 5;
        slice_mut[1] += 5;
    } // `slice_mut` goes out of scope here!

    // `data` can now be borrowed immutably or mutated freely:
    data.push(40);
    println!("data = {data:?}");
}
data = [15, 25, 30, 40]

Items Declared Inside Block Expressions

In addition to let statements and expression statements, a block expression can contain local item declarations—including fn, struct, enum, const, type, and use statements! Items declared inside a block are scoped strictly to that block:

fn main() {
    let scaled = {
        const FACTOR: i32 = 10;

        struct Pair(i32, i32);

        fn sum_pair(p: Pair) -> i32 {
            (p.0 + p.1) * FACTOR
        }

        sum_pair(Pair(3, 4))
    };

    println!("scaled = {scaled}");
}
scaled = 70

Early Exit from a Block Expression ('label: { ... })

By attaching a label 'label: to a block expression, you can exit the block early from an inner if or match using break 'label value; without returning from the enclosing function:

fn main() {
    let override_val: Option<i32> = None;

    let result: i32 = 'compute: {
        if let Some(v) = override_val {
            break 'compute v; // Exits `'compute` block immediately with `v`
        }

        let base = 20;
        base * 2 + 2 // Normal tail expression of `'compute`
    };

    println!("result = {result}");
}
result = 42

Block Expressions as Function Arguments and Arithmetic Operands

Because a { ... } block is a first-class expression, it can appear anywhere a value expression is expected—including inside function arguments or arithmetic expressions:

fn main() {
    let total = 100 + {
        let bonus = 25;
        bonus * 2
    };

    println!("total = {total}");
}
total = 150