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 isTand compile-time length isN. - Array repeat expression
[value; N]: Creates an array of lengthNinitialized with copies ofvalue.
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.