1.11Expressions and statements

Last updated June 11, 2026

This is the most important lesson in the chapter. It looks like grammar trivia. It is actually the master key to Rust's design, and chapters from here to the end of the course will quietly lean on it.

Expressions

An expression is any piece of code that produces a value. Working out that value is called evaluating the expression. You've been writing expressions all chapter:

Expressions nest: in (x + 1) * 2, the inner expression's value feeds the outer one. Wherever Rust expects a value, any expression of the right type can stand there; that substitution rule is what makes them composable, like Lego studs.

Statements

A statement (lesson 1.1's oldest term, now in sharper focus) is an instruction that performs an action but doesn't produce a value for surrounding code. The let definition is the statement you know best: let x = 5; contains the expression 5, but the statement as a whole just performs the action "create x". You can't write let y = (let x = 5);, because there's no value there to use.

And here's the bridge between the two worlds: take any expression, add a semicolon, and you get an expression statement: the expression is evaluated, and its value is thrown away.

x + 1;

That's legal Rust. It computes one more than x, discards the result, and accomplishes nothing (the compiler will warn you it's pointless). Which reveals the semicolon's true job in Rust. It isn't just a sentence-ending period; it's the discard marker. println!("hi"); is an expression statement too: do the printing, discard the (useless) result, move on.

So what's the result of a println!? That needs one more character of vocabulary.

The unit type

Rust has a type for "nothing useful here," written () and called the unit type. It has exactly one value, also written (). Printing produces it. Assignment produces it (this is why a = b = 5 from last lesson isn't a thing). Any expression that exists for its side effects, rather than to compute something, evaluates to ().

You'll rarely write () on purpose this early, but you'll see it in error messages constantly, and now you can read it: "found ()" means "that thing produced nothing useful."

Blocks are expressions

Now the payoff. A pair of braces containing code is a block, and in Rust, a block is an expression. Its value is the value of the final expression inside it, the one without a semicolon, called the block's tail expression:

fn main() {
    let y = {
        let a = 2;
        a * 3
    };
    println!("{y}");
}
6

Read the block like Rust does: run the statements in order (let a = 2;), then evaluate the tail expression (a * 3, no semicolon) and hand that value out as the block's result. The whole { ... } evaluated to 6, so y is 6.

Now watch one semicolon change the meaning. Put one after a * 3 and there's no tail expression anymore, just statements; the block's value becomes (), and the program stops compiling, because y is now () and println! has no idea how to display "nothing useful":

error[E0277]: `()` doesn't implement `std::fmt::Display`
 --> src/main.rs:6:15
  |
6 |     println!("{y}");
  |               ^^^ `()` cannot be formatted with the default formatter
  |
  = help: the trait `std::fmt::Display` is not implemented for `()`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

(The message mentions traits and formatters, chapter 16 machinery, but you can already read the headline: y is (), and () isn't printable text.)

Warning

Semicolon discipline, then: inside a block, a semicolon at the end of the last line is the difference between "this block produces that value" and "this block produces ()". A stray or missing final semicolon is about to become your most common one-character bug, and now you know exactly what it does.

This also pays off the trick question from lesson 1.1's quiz. fn main() { println!("Hi there") } compiles because the println! with no semicolon is the body's tail expression. Its value is (), and () happens to be exactly what main is expected to produce. No semicolon, no crime, by complete coincidence of types.

Key insight

Languages in the C family are statement-oriented: code is mostly instructions, and values live only inside them. Rust is expression-oriented: nearly everything produces a value, including blocks and (as you'll see) entire ifs and matches. The practical habit to build is asking not just "what does this code do?" but "what does this code evaluate to?" Lesson 4.7 will let you write let x = if ... ; lesson 2.2 will show function bodies are just blocks whose tail expression is the return value. Both are this lesson wearing different hats.

Quiz time

Question #1

Classify each line: statement (not an expression statement), expression statement, or expression?

a) let width = 80; b) width + 20; c) width + 20 d) println!("{width}");

Show solution

a) A statement (a let definition; produces no value). b) An expression statement: evaluates width + 20, then the semicolon discards the 100. (Pointless, and the compiler warns about it.) c) An expression. On its own line it could be a block's tail expression, producing 100. d) An expression statement: prints, then discards the ().

Question #2

What does this program print?

fn main() {
    let x = 4;
    let y = {
        let x = x + 1;
        x * 2
    };
    println!("{x} {y}");
}
Show solution
4 10

Inside the block, a new x (worth 5) exists, and the tail expression x * 2 makes the block evaluate to 10. The outer x is untouched, still 4. (Two variables named x? That's shadowing, lesson 5.2, but the block-value machinery is pure 1.11.)

Question #3

Predict the compiler error:

fn main() {
    let z = {
        let a = 6;
        a + 1;
    };
    println!("{z}");
}
Show solution

The semicolon after a + 1 means the block has no tail expression, so it evaluates to (), so z is (), and println!("{z}") fails with E0277: () doesn't implement Display. Delete that one semicolon and the program prints 7.

That's the deep idea of the chapter in your pocket. Time to spend it: in the next lesson you'll build a complete program from scratch, wrong turns included.