Rust (2024 edition) Module 2 – Values, types, functions and control flow
Functions, blocks and expressions
Write Rust functions with typed parameters and results, use blocks and if as values, return early, and see why one stray semicolon breaks a function.
What you will learn
- Define functions with typed parameters and a result type, and call them from anywhere in their file
- Distinguish statements from expressions, and use a block or an if as a value
- Fix error E0308 caused by a semicolon after a function's last expression
- Use return to leave a function early, and predict what panic!, todo! and process::exit do instead of returning
- Write a function in expression style, without mutable variables or a final return
Before you start
What you’ll learn · Before you start
What you will learn
- Define functions with typed parameters and a result type, and call them from anywhere in their file
- Distinguish statements from expressions, and use a block or an if as a value
- Fix error E0308 caused by a semicolon after a function's last expression
- Use return to leave a function early, and predict what panic!, todo! and process::exit do instead of returning
- Write a function in expression style, without mutable variables or a final return
Before you start
On this page
Remove one character from a three-line Rust function and it compiles; put it back and rustc reports E0308,
mismatched types: expected u32, found (). The character is a semicolon, and the error makes sense once you know
that almost everything in Rust is an expression that has a value.
This lesson writes functions with typed parameters and results, uses blocks and if as values, and returns early
when an input makes no sense, with a train fare worked out from distance bands as the running example. The fares are
made up for the lesson, and every result is a run on Rust 1.99.0.
A function states the type of every parameter and of its result
Rust functions begin with fn, then the name, the parameters each with its type, and, after ->, the type of the
value the function returns. This program defines three functions and calls each one:
// Three functions: one with a result, one with two results, and one with none.
fn platform_fee(adults: u32, children: u32) -> u32 {
adults * 1_000 + children * 500
}
fn main() {
let fee = platform_fee(2, 1);
println!("platform fee: {fee} paise");
let (each, left_over) = split(fee, 3);
println!("split three ways: {each} paise each, {left_over} left over");
announce(fee);
}
// Defined after main, which calls it: the order of functions in a file does not matter.
fn split(total: u32, people: u32) -> (u32, u32) {
(total / people, total % people)
}
fn announce(fee: u32) {
println!("announced: the fee is {fee} paise");
} Output
platform fee: 2500 paise split three ways: 833 paise each, 1 left over announced: the fee is 2500 paise
Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O functions.rs && ./functions
platform_fee takes two u32 values and returns one. split returns two values at once, as a tuple, so the caller
takes them apart with a pattern. announce has no -> at all, which means it returns (), the unit value. And
main calls split and announce before their definitions in the file: functions are items, and the compiler finds
them wherever they are in their module. The types of parameters are never inferred, and a missing one is a syntax
error:
// A parameter without its type.
fn platform_fee(adults) -> u32 {
adults * 1_000
}
fn main() {
println!("{}", platform_fee(2));
} Output (exit status 1)
Printed as an error (standard error)
error: expected one of `:`, `@`, or `|`, found `)`
--> untyped_param.rs:2:23
|
2 | fn platform_fee(adults) -> u32 {
| ^ expected one of `:`, `@`, or `|`
|
help: if this is a parameter name, give it a type
|
2 | fn platform_fee(adults: TypeName) -> u32 {
| ++++++++++
help: if this is a type, explicitly ignore the parameter name
|
2 | fn platform_fee(_: adults) -> u32 {
| ++
error: aborting due to 1 previous error
Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O untyped_param.rs && ./untyped_param
The help suggests the fix, a type after the name. Requiring types on every parameter is deliberate: rustc can then check each function on its own, and a reader learns what it takes from its first line. A name also means one function: Rust has no overloading by parameter types and no default values for parameters.
A block is an expression, and its last expression is its value
Rust distinguishes statements, which do something, from expression: A piece of code that produces a value, such as 2 + 3, a function call, an if or a block. A block's last expression, written without a semicolon, is the value of the whole block., which produce a
value. let base = 2_000; is a statement; base + extra is an expression; and blocks are expressions too, made of
statements and an optional last expression without a semicolon, the block’s value:
// A block and an if, each used as a value.
fn main() {
let km: u32 = 120;
let fare = {
let base = 2_000;
let extra = (km - 50) * 60;
base + extra
};
let kind = if km > 50 { "regional" } else { "local" };
println!("{km} km: {kind}, fare {fare} paise");
} Output
120 km: regional, fare 6200 paise
Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O blocks.rs && ./blocks
fare got its value, 6200, from the block’s last line, and kind got "regional" from an if, which is an
expression as well: each branch is a block, and the branch that runs gives the if its value. So both branches must
have the same type. Step through how the block becomes a value, and what one semicolon would change:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A statement.
let base = 2_000;is a statement: it binds base to 2000 and has no value of its own. The block goes on to the next line. - Another statement.
let extra = (km - 50) * 60;works out (120 − 50) × 60 = 4200 and binds it. Aletis always a statement. - The tail expression.
base + extrahas no semicolon after it, so it is the block's tail: its value, 6200, becomes the value of the whole block. - The block is a value. The block stands where a value is expected, after
let fare =, so fare becomes 6200. The names base and extra end with the block; only its value survives. - One semicolon more. Write
base + extra;and it becomes a statement too. The block then ends without a tail, and its value is(), the unit value; in a function that promised au32, that is the E0308 of semicolon.rs.
A function body is a block, so the same rule decides what a function returns. With a semicolon after its last
expression, the body ends in a statement, and its value is ():
// A reservation charge added to a fare, with one character too many.
fn with_reservation(fare: u32) -> u32 {
fare + 4_000;
}
fn main() {
println!("{}", with_reservation(6_200));
} Output (exit status 1)
Printed as an error (standard error)
error[E0308]: mismatched types
--> semicolon.rs:2:35
|
2 | fn with_reservation(fare: u32) -> u32 {
| ---------------- ^^^ expected `u32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
3 | fare + 4_000;
| - help: remove this semicolon to return this value
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O semicolon.rs && ./semicolon
rustc says both halves of the story: the function “implicitly returns () as its body has no tail or return
expression”, and the help points at the semicolon to remove.
Ending a function's value with a semicolon
fare + 4_000; throws the sum away and leaves the function with (). Write the last expression without a
semicolon, fare + 4_000, and it becomes the function’s result.
Checkpoint
Check: Blocks and if as values
return leaves a function early
Most functions end by producing their last expression, but some inputs should stop a function before it gets there.
This fare function refuses a distance of 0 or more than 5,000 km with return, and works out the rest with an if
chain whose value becomes the fare:
// A fare by distance band, with an early return for a distance that makes no sense.
fn fare_paise(km: u32) -> Option<u32> {
if km == 0 || km > 5_000 {
return None; // nothing sensible to charge
}
let fare = if km <= 50 {
2_000
} else if km <= 200 {
2_000 + (km - 50) * 60
} else {
11_000 + (km - 200) * 45
};
Some(fare)
}
fn main() {
for km in [0, 30, 120, 260, 6_000] {
println!("{km:>5} km: {:?}", fare_paise(km));
}
} Output
0 km: None 30 km: Some(2000) 120 km: Some(6200) 260 km: Some(13700) 6000 km: None
Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O early.rs && ./early
The result type, Option<u32>, says the function gives either Some(fare) or None, no fare at all; the module on
enums covers Option in full. return None; leaves the function at once with that value. For every other distance,
the if chain is an expression whose value is bound to fare, and Some(fare), the last expression, is the result.
That is the usual shape of Rust functions: return for the early exits, and a last expression, without return, for
the normal result.
Some expressions never produce a value at all. panic! stops the thread, todo!() panics with “not yet
implemented”, and std::process::exit ends the whole process. Rust gives them the never type, !.
Key idea
A function’s result is its body’s last expression, written without a semicolon; return is for leaving early,
and an if or a block can produce the value itself.
Checkpoint
Check: Returning early · Blocks and if as values
Expressions that never return, and what they skip Pro: opens with a Pro, Premium or Ultimate pass
Some expressions never produce a value: they stop the thread or end the whole process. This part runs three of
them, shows why they fit wherever a value is expected, and measures what ending a program with process::exit costs.
In this part:
- Three ways to never return, run
- Why if loaded { 2_000 } else { panic!(…) } has a type
- What process::exit costs a program with a buffered file, measured
- Choosing between a panic, an exit and a returned error
4 runnable examples (Python, Rust) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Expression style, Rust's answer to overloading, and the fare exercise Pro: opens with a Pro, Premium or Ultimate pass
Idiomatic Rust lets values come out of blocks and if expressions, and keeps return for early exits. This part runs
one function through Clippy, shows what Rust APIs offer instead of overloading and default arguments, and ends with
an exercise in expression style.
In this part:
- The same fare in two styles, and what Clippy and rustc say
- No overloading or default arguments: what Rust APIs do instead
- Functions inside functions, and functions the compiler runs
- Exercise: a fare by band and class, with no mutable variables
5 runnable examples (Python, Rust) · about 5 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise: functions, blocks and early returns
These questions are about function signatures, blocks and if as values, the stray semicolon and early returns.
Every choice comes with the reason it is right or wrong.
Practice that adapts to you
Practise this lesson
11 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): what is the difference between a statement and an expression in Rust?
An expression produces a value: 2 + 3, a function call, an if, a block. A statement does something and has no
value of its own: a let statement binds a name, and an expression followed by a semicolon is an expression
statement, whose value is thrown away. Rust is expression-based, so a block’s last expression, written without a
semicolon, is the block’s value, and a function returns its body’s last expression without return. That is why
x + 1; at the end of a function that promises a u32 is error E0308: the semicolon turns the result into a
statement, and the body’s value becomes ().
-
Mid to senior
Rust has no default arguments and no overloading. A fare function is usually called with a distance only, sometimes with a class, and rarely with a class, a concession and a travel date. How would you shape its API, and why?
-
Mid to senior
A command-line tool writes a CSV report through a BufWriter. Deep inside, a parse function calls std::process::exit(1) on a malformed row. Users say the report is sometimes empty, even of rows before the bad one. Why, and how would you restructure the error path?
-
Fresher to mid
A teammate writes
fn fare(km: u32) -> u32that callspanic!("bad distance")when km is 0, because "it should never happen". The function is called from a booking API. What would you change, and when is a panic the right call?
Key takeaways
- Rust functions write out every parameter’s type and their result’s after
->; without->they return(). - Blocks and
ifare expressions: a block’s value is its last expression without a semicolon, and both branches of anifmust have the same type. - A semicolon after a function’s last expression makes its value
(), which is error E0308 when the function promises another type. returnleaves a function early; the normal result is the last expression, andpanic!,todo!()andprocess::exitnever return at all.
Terms in this lesson
- expression
- A piece of code that produces a value, such as 2 + 3, a function call, an if or a block. A block's last expression, written without a semicolon, is the value of the whole block.
References
- The Rust Programming Language: Functions (The Rust Project)
- The Rust Reference: Functions (The Rust Project)
- The Rust Reference: Statements and expressions (The Rust Project)
- The Rust Reference: Block expressions (The Rust Project)
- The Rust Reference: if expressions (The Rust Project)
- The Rust Reference: Return expressions (The Rust Project)
- The Rust Reference: Never type (The Rust Project)
- The Rust Reference: Constant evaluation (const functions) (The Rust Project)
- Rust error codes index: E0308 (The Rust Project)
- Rust error codes index: E0428 (The Rust Project)
- The Rust Standard Library: std::process::exit (The Rust Project)
- The Rust Standard Library: std::process::ExitCode (The Rust Project)
- The Rust Standard Library: std::io::BufWriter (The Rust Project)
- The Rust Standard Library: todo! (The Rust Project)
- Clippy lints (The Rust Project)
- Rust API Guidelines: Naming (The Rust Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress