Functions & Expressions
A function is a named block of code that does one job. You have already used main, the function where every program starts. Real programs split their work into many small functions, each easy to read and test.
Rust functions look familiar if you know C, Java or JavaScript, but they hide one important idea: almost everything in Rust is an expression that produces a value, and a single semicolon can change what a function returns. This lesson explains both.
Defining and calling functions
A function starts with fn, then its name in snake_case, its parameters in brackets and a body in curly brackets. Every parameter must have a type; Rust never guesses parameter types.
fn main() {
greet("Farhan", 21);
greet("Meera", 19);
}
fn greet(name: &str, age: u32) {
println!("Hello {name}, you are {age} years old.");
}Hello Farhan, you are 21 years old. Hello Meera, you are 19 years old.
greet is defined after main, and that is fine: Rust does not care about the order of functions in a file. The values you pass in a call ("Farhan", 21) are called arguments; the names in the definition (name, age) are parameters. The arguments must match the parameter types exactly, so greet("Farhan", -21) does not compile, because u32 cannot be negative.
Rust has no default parameter values and no overloading (two functions with the same name). If you need a variant, give it a different name, such as greet_formal.
Returning a value
The return type comes after an arrow ->. The value of the last expression in the body, written without a semicolon, is returned automatically:
fn total_with_gst(price_paise: u64) -> u64 {
let gst = price_paise * 18 / 100;
price_paise + gst
}
fn main() {
let total = total_with_gst(129_900);
println!("Total: {total} paise");
println!("That is Rs. {}.{:02}", total / 100, total % 100);
}Total: 153282 paise That is Rs. 1532.82
The price is kept in paise (a whole number) so no rounding errors creep in. {:02} prints a number with at least two digits, padding with a zero, so 5 paise would show as 05.
Statements and expressions
Rust code is made of two kinds of pieces:
- A statement performs an action and produces no value.
let gst = 10;is a statement, solet a = (let b = 5);is an error. - An expression evaluates to a value.
5 + 3, a function call,ifand a block{ ... }are all expressions.
Adding a semicolon to the end of an expression turns it into a statement and throws its value away. A block's value is its final expression, which lets you compute something in a few steps without a separate function:
fn main() {
let overs = 7;
let balls = {
let full_overs = overs * 6;
full_overs + 3
};
println!("Balls bowled: {balls}");
}Balls bowled: 45
The inner block's last line full_overs + 3 has no semicolon, so it becomes the value of the whole block, and full_overs disappears at the closing bracket.
The semicolon mistake
Here is the most common error new Rust programmers see. The last line ends with a semicolon, so the function body has no final expression:
fn gst_amount(price: u32) -> u32 {
price * 18 / 100;
}error[E0308]: mismatched types
--> src/main.rs:1:30
|
1 | fn gst_amount(price: u32) -> u32 {
| ---------- ^^^ expected `u32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
2 | price * 18 / 100;
| - help: remove this semicolon to return this valueThe body now returns (), the unit type that means "nothing", but the signature promised a u32. The compiler even points at the semicolon to remove. Remember the rule: the last expression, without a semicolon, is the return value.
Early return and returning several values
return leaves a function immediately. Use it for early exits, such as rejecting bad input at the top. To return more than one value, return a tuple:
fn metro_fare(age: u32) -> u32 {
if age < 5 {
return 0; // small children travel free
}
let base = 40;
if age >= 60 { base / 2 } else { base }
}
fn split_bill(amount: u32, people: u32) -> (u32, u32) {
(amount / people, amount % people)
}
fn main() {
println!("Fare at 3: {}", metro_fare(3));
println!("Fare at 25: {}", metro_fare(25));
println!("Fare at 67: {}", metro_fare(67));
let (each, left_over) = split_bill(1000, 3);
println!("Each friend pays {each}, {left_over} rupee left over");
}Fare at 3: 0 Fare at 25: 40 Fare at 67: 20 Each friend pays 333, 1 rupee left over
In metro_fare, the final if ... else ... is an expression, so its value (20 or 40) is the return value. Both branches must have the same type. The caller unpacks the tuple from split_bill with destructuring, as you saw in Scalar & Compound Types.
Documenting functions
Ordinary comments start with //. Doc comments start with /// and describe the item below them. Tools such as cargo doc and your editor show them, and any code examples inside them can be run as tests (see Testing: Unit, Integration & Doc Tests).
/// Returns the number of whole pages needed to print `lines` lines,
/// when one page holds `per_page` lines.
fn pages_needed(lines: u32, per_page: u32) -> u32 {
lines.div_ceil(per_page)
}div_ceil divides and rounds up, so pages_needed(101, 50) is 3.
Common mistakes
- A semicolon after the final expression. The function then returns
()and you get "mismatched types". Remove the semicolon. - Leaving out parameter types.
fn area(w, h)is an error; writefn area(w: u32, h: u32) -> u32. ifbranches with different types.if x { 1 } else { "one" }cannot be one value.- Expecting default arguments. Rust has none; use separate functions or pass an
Option.
Key points
- Functions are declared with
fn, parameters always have types, and the return type follows->. - The last expression without a semicolon is the function's return value;
returnis for early exits. - Statements perform actions; expressions produce values. Blocks and
ifare expressions. - A semicolon after the final expression makes the function return
(), causing a type error. - Return several values with a tuple, and document functions with
///doc comments.
Exercise
Write a function electricity_bill(units: u32) -> u32 for a town in Kochi: the first 100 units cost 4 rupees each and every unit above 100 costs 6 rupees. Use an early return for bills of 100 units or fewer. Call it from main for 80 units and for 250 units and print both bills.
Show solution
fn electricity_bill(units: u32) -> u32 {
if units <= 100 {
return units * 4;
}
let extra_units = units - 100;
100 * 4 + extra_units * 6
}
fn main() {
println!("80 units: Rs. {}", electricity_bill(80));
println!("250 units: Rs. {}", electricity_bill(250));
}80 units: Rs. 320 250 units: Rs. 1300
For 250 units the first 100 cost 400 and the remaining 150 cost 900, giving 1300. The final line has no semicolon, so its value is returned.
Quick Quiz
1. What does a function return if its last line is a + b; (with a semicolon) and it has no return?
The semicolon turns the expression into a statement, so the body has no final expression and returns ().
2. Which signature is valid Rust?
Parameters are written name: Type, and the return type follows ->.
3. What is the value of x after let x = { let y = 4; y * 2 };?
The block's final expression y * 2 has no semicolon, so it becomes the block's value.
4. How can a Rust function return two values?
Return a tuple and destructure it at the call site.
Learn Rust with a mentor
Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
