Start learning free

LESSON 52 OF 414 · FUNCTIONS I

Blocks Have Values in Rust: Expressions and Return Values

Lesson Read the full explanation

In Rust, almost everything is an expression — a piece of code that produces a value. Even a whole { ... } block is an expression: it evaluates to its last expression, as long as that last expression has no semicolon.

let y = {
    let a = 3;
    a + 1       // no semicolon: this value flows out of the block
};
// y is now 4

Functions work exactly the same way: a function's body is a block, so a function returns its last expression automatically. You only need the return keyword for early exits.

fn add_one(n: i32) -> i32 {
    n + 1       // this is the return value — no `return`, no semicolon
}

The trap: one accidental semicolon

A semicolon turns an expression into a statement, and a statement's value is () — the empty "unit" type. So writing n * n; as the last line makes the function return () instead of a number, and the compiler stops you:

error[E0308]: mismatched types
 --> trap.rs:1:22
  |
1 | fn square(n: i32) -> i32 {
  |    ------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
2 |     n * n;
  |          - help: remove this semicolon to return this value

error: aborting due to 1 previous error

Read the help line — the compiler even tells you the fix: remove this semicolon to return this value. Every Rust programmer hits this error; now you know exactly what it means.


Assignment
Follow the Coding Tutorial:

  • Define a function square(n: i32) -> i32 whose body is just n * n — no return, no semicolon.
  • Bind doubled to a block that declares let x = 21; and ends with the expression x * 2.
  • Print both results.

The expected output is:

doubled = 42
square(6) = 36

Hint
If you see expected `i32`, found `()`, look for a semicolon on the line that should produce the value.


STEP 1 OF 7
    Ctrl + Enter to check

    RUST · FUNCTIONS I

    Expression Returns in Rust, explained

    Expressions and statements

    In Rust, almost everything is an expression: a piece of code that produces a value. 3 + 4 is an expression, a function call is an expression, and so is an if with its branches. A whole block in curly braces is an expression too. It evaluates to its last expression, as long as that last expression has no semicolon.

    let y = {
        let a = 3;
        a + 1       // no semicolon: this value flows out of the block
    };
    println!("{}", y);  // 4
    

    Inside the block, let a = 3; is a statement. It does some work and produces no value of its own. The final line a + 1 is an expression, and because nothing follows it, its value becomes the value of the entire block. Note that the variable a lives only inside the braces. It can't be used after the closing };.

    The same rule applies to if, which is why Rust needs no ternary operator:

    let size = 7;
    let label = if size > 5 { "big" } else { "small" };
    println!("{}", label);  // big
    

    Functions return their last expression

    A function body is a block, so a function returns its last expression automatically. The keyword return is only needed for early exits from the middle of a function.

    fn add_one(n: i32) -> i32 {
        n + 1       // the return value: no `return`, no semicolon
    }
    

    The arrow -> i32 promises that the body produces an i32. The compiler then checks that the last expression really has that type.

    The trap: one accidental semicolon

    A semicolon turns an expression into a statement, and a statement's value is (), the empty "unit" type. If you write n * n; as the last line of a function that promises an i32, the function ends up returning (), and the compiler stops you:

    error[E0308]: mismatched types
      expected `i32`, found `()`
      implicitly returns `()` as its body has no tail or `return` expression
      help: remove this semicolon to return this value

    The compiler even states the fix: remove the semicolon. The same thing happens with blocks. If the last line of a block ends with a semicolon, the block's value is ():

    let unit = { let y = 5; y + 1; };
    println!("{:?}", unit);  // ()
    

    Here the addition is computed and thrown away, and the compiler warns about the unused result.

    The exercise, step by step

    The exercise asks for a function square, a block bound to doubled, and two printed results:

    doubled = 42
    square(6) = 36
    
    1. Declare the helper. fn square(n: i32) -> i32 with its closing brace. The arrow commits the body to producing an i32, so the compiler can check the next step.
    2. Write the body as a bare expression. n * n, with no return and no semicolon. A trailing ; would produce the mismatched types error above.
    3. Open a let doubled = { ... }; block. The closing brace needs the ; after it, because that semicolon ends the let statement.
    4. Declare a local inside the block. let x = 21; ends with a semicolon, as every let does.
    5. End the block with a tail expression. x * 2 without a semicolon. Its value, 42, flows out into doubled.
    6. Print doubled. The format string is "doubled = {}", and the {} placeholder is filled with the value after the comma. Here doubled is a plain i32, which proves the block evaluated to a value.
    7. Print the helper's result. square(6) goes right inside the arguments. A call is an expression, so it can sit anywhere a value is expected, with no variable needed first.
    fn square(n: i32) -> i32 {
        n * n
    }
    let doubled = {
        let x = 21;
        x * 2
    };
    println!("doubled = {}", doubled);
    println!("square(6) = {}", square(6));
    

    Common mistakes

    • A semicolon after the tail expression. It turns the value into (). If you see expected i32, found (), look for a semicolon on the line that should produce the value.
    • Forgetting the semicolon after the closing brace of a let. The statement let doubled = { ... } needs it.
    • Using a block's local variable outside it. x is gone after the closing brace.
    • Using return inside a block expression by habit. It leaves the whole function, not just the block.

    Key takeaways

    • A block evaluates to its last expression when that expression has no semicolon.
    • A function returns its last expression, so return is for early exits.
    • Adding a semicolon to a would-be value turns it into a statement worth (), which is the cause of the mismatched types error.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free