Hello World and Program Structure

Semicolons in Rust

A semicolon (;) in Rust is not passive line punctuation. In an expression-oriented language, a semicolon actively separates statements, terminates let bindings and item declarations, converts value-producing expressions into ()-producing statements, and separates the element type from the length in fixed-size array syntax.

Roles of the Semicolon

Depending on where it appears in a Rust file, ; performs one of four syntactic roles:

Context Example Role of ;
let binding statement let x = 10; Terminates the variable declaration statement (always required)
Expression statement compute(); Discards the expression's value and makes the statement evaluate to ()
Item declaration use std::io; Ends an item that does not have a brace-delimited body ({ ... })
Array type / repeat literal [i32; 4] / [0; 4] Separates the element type or initial value from the compile-time length

Semicolons vs Tail Expressions in Blocks

Inside any block { ... }, a semicolon at the end of the final expression determines what value the block produces.

  • Without a trailing semicolon, the final expression is the block's tail expression, and the block evaluates to that expression's value and type.
  • With a trailing semicolon, the final expression becomes an expression statement, its value is dropped, and the block evaluates to the unit value () of type ().
Block without trailing semicolon:           Block with trailing semicolon:
+-------------------------------+           +-------------------------------+
| {                             |           | {                             |
|     let base = 10;            |           |     let base = 10;            |
|     base * 2                  |           |     base * 2;                 |
| }                             |           | }          ^                  |
+---------------+---------------+           +------------+------------------+
                |                                        |
                v                                        v
     Evaluates to 20 (i32)                    20 is discarded; evaluates to ()

Both forms are valid when the surrounding context accepts their resulting type:

fn main() {
    let value: i32 = {
        let base = 10;
        base * 2
    };

    let nothing: () = {
        let base = 10;
        base * 2;
    };

    println!("value = {value}, nothing = {nothing:?}");
}
value = 20, nothing = ()

Semicolons in Function Bodies

A function body is a block whose tail expression must match the function's declared return type. Adding a semicolon to the final expression of a function that returns a non-() type causes a compile-time type mismatch:

// ❌ does not compile
fn square(n: i32) -> i32 {
    n * n;
}

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

error: aborting due to 1 previous error

Conversely, if a function omits a return type (meaning its return type defaults to ()), any final expression that does not evaluate to () must end with a semicolon so its value is discarded:

// ❌ does not compile
fn log_computation(x: i32) {
    x + 1
}

fn main() {
    log_computation(5);
}
error[E0308]: mismatched types
 --> src/main.rs:3:5
  |
2 | fn log_computation(x: i32) {
  |                           - help: try adding a return type: `-> i32`
3 |     x + 1
  |     ^^^^^ expected `()`, found `i32`

error: aborting due to 1 previous error

Semicolons with return, break, and continue

Expressions that diverge—such as return value, break, continue, and panic!()—have the never type (!), which coerces into any type. Because ! satisfies both () and any concrete return type, a trailing semicolon after return expr is optional:

fn early_check(x: i32) -> i32 {
    if x < 0 {
        return 0; // Semicolon is conventional here
    }
    return x * 2 // Compiles with or without `;`, though `x * 2` without `return` is idiomatic
}

fn main() {
    println!("{}", early_check(5));
}
10

Control-Flow Expressions and Semicolons

Rust classifies expressions into two grammar categories: expressions with blocks (if, match, loop, while, for, and bare { ... } blocks) and expressions without blocks (function calls, arithmetic, method calls, literals).

Standalone Statement Position

When an expression with a block is used as a standalone statement that returns (), no semicolon is needed after its closing brace }:

fn main() {
    let temperature = 25;

    if temperature > 20 {
        println!("Warm");
    } // No semicolon needed here

    println!("Done");
}
Warm
Done

However, if an if or match expression produces a non-() value and appears in the middle of a block (not as the block's tail expression), it cannot stand alone without a semicolon unless its result is used or explicitly discarded:

// ❌ does not compile
fn main() {
    if true {
        10
    } else {
        20
    }

    println!("After if");
}
error[E0308]: mismatched types
 --> src/main.rs:3:9
  |
3 | /     if true {
4 | |         10
  | |         ^^ expected `()`, found integer
5 | |     } else {
6 | |         20
7 | |     }
  | |_____- expected this to be `()`
  |
help: you might have meant to return this value or assign it to a variable
  |
3 |     let _ = if true {
4 |         10
5 |     } else {
6 |         20
7 |     };
  |

error: aborting due to 1 previous error

Inside let Statements

Whenever a block, if, match, or loop expression is bound to a variable using let, the outer statement is a let declaration statement, not a block expression. Therefore, a semicolon is mandatory after the closing brace }:

fn main() {
    let status = if 42 % 2 == 0 {
        "even"
    } else {
        "odd"
    }; // Mandatory: terminates the `let` statement

    let computed = {
        let a = 3;
        let b = 4;
        a * a + b * b
    }; // Mandatory: terminates the `let` statement

    println!("{status}: {computed}");
}
even: 25

Omitting the ; after } in a let binding causes a syntax error on the next token:

// ❌ does not compile
fn main() {
    let status = if true { "on" } else { "off" }
    println!("{status}");
}
error: expected `;`, found `println`
 --> src/main.rs:2:49
  |
2 |     let status = if true { "on" } else { "off" }
  |                                                 ^ help: add `;` here
3 |     println!("{status}");
  |     ------- unexpected token

error: aborting due to 1 previous error

Semicolons in Item Declarations

Top-level and module-level items follow a consistent rule: items that end with a brace block { ... } do not take a trailing semicolon, whereas items without a brace block require a trailing semicolon.

Item Kind Syntax Trailing ;
Function definition fn add(a: i32, b: i32) -> i32 { a + b } Forbidden
Trait method signature (no body) fn area(&self) -> f64; Required
Named-field struct struct Point { x: f64, y: f64 } Forbidden
Tuple struct struct Meters(f64); Required
Unit-like struct struct Marker; Required
Enum definition enum Color { Red, Blue } Forbidden
Constant (const) const MAX: usize = 100; Required
Static variable (static) static COUNT: i32 = 0; Required
Type alias (type) type Kilometers = i32; Required
Use import (use) use std::collections::HashMap; Required
Inline module mod network { ... } Forbidden
External file module mod network; Required
Macro invocation with () or [] println!("hi"); Required in statement/item position
Macro invocation with {} thread_local! { ... } Optional / omitted
struct UnitMarker;
struct Wrapper(i32);
struct Pair {
    first: i32,
    second: i32,
}

const DEFAULT_VALUE: i32 = 7;
type Score = i32;

fn main() {
    let _m = UnitMarker;
    let w = Wrapper(DEFAULT_VALUE);
    let p = Pair {
        first: w.0,
        second: 10 as Score,
    };
    println!("{} and {}", p.first, p.second);
}
7 and 10

Semicolons in Array Types and Repeat Expressions

Outside of statement termination, ; is used inside square brackets [...] to distinguish fixed-size array specifications from comma-separated element lists:

  • Array type signature [T; N]: Declares a fixed-size array whose element type is T and compile-time length is N.
  • Array repeat expression [value; N]: Creates an array of length N initialized with copies of value.
fn main() {
    let listed: [i32; 4] = [1, 2, 3, 4]; // Comma separates individual elements
    let repeated: [i32; 4] = [0; 4];     // Semicolon repeats `0` four times

    println!("listed:   {listed:?}");
    println!("repeated: {repeated:?}");
}
listed:   [1, 2, 3, 4]
repeated: [0, 0, 0, 0]

Writing a comma instead of a semicolon in a type annotation ([i32, 4]) is a syntax error, whereas writing [0, 4] creates a two-element array containing 0 and 4 rather than four zeros.

Empty Statements and Redundant Semicolons

A standalone ; inside a block is a syntactically valid empty statement that does nothing. However, rustc emits an unused_must_use or redundant_semicolons lint warning when extra semicolons appear in a block:

fn main() {
    let x = 5;;
    println!("{x}");
}
warning: unnecessary trailing semicolon
 --> src/main.rs:2:15
  |
2 |     let x = 5;;
  |               ^ help: remove this semicolon
  |
  = note: `#[warn(redundant_semicolons)]` on by default

Discarding #[must_use] Values with a Semicolon

When an expression statement ends with ;, its returned value is silently dropped. If the expression's type or function is marked with #[must_use] (such as Result<T, E> or pure arithmetic methods like checked_add), discarding it with a semicolon triggers a compiler warning to prevent accidentally ignored errors:

fn main() {
    10_i32.checked_add(20);
}
warning: unused return value of `num::<impl i32>::checked_add` that must be used
 --> src/main.rs:2:5
  |
2 |     10_i32.checked_add(20);
  |     ^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
  |
2 |     let _ = 10_i32.checked_add(20);
  |     +++++++

Assigning to the wildcard pattern (let _ = expr;) explicitly tells the compiler that discarding the value is intentional.