Functions
Beginner · Language basics
What & why
A function is a named chunk of code you can run whenever you want, as many times as you want. Instead of copying the same ten lines everywhere, you put them in a function and call it by name. You’ve already met one function on day one: main. Now you’ll learn to write your own — to give them inputs and get answers back.
The idea, slowly
The shape of a function
Here’s a function that adds two numbers:
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let sum = add(2, 3);
println!("sum is {sum}"); // 5
}
Let’s read the first line the way the compiler does:
fn add—fnmeans “I’m defining a function,” andaddis its name.(a: i32, b: i32)— these are the parameters: the inputs. Each one needs a name and a type.ais ani32,bis ani32. Rust never guesses parameter types — you must always write them.-> i32— the arrow says “this function hands back ani32when it’s done.” This is the return type. If a function returns nothing, you leave the arrow off entirely.{ a + b }— the body. This function’s whole job is to computea + b.
Then in main, add(2, 3) calls the function: it runs add with a = 2 and b = 3, and the answer comes back and lands in sum.
The big idea: expressions vs statements
This is the part that trips up newcomers, so go slow. Rust code is made of two things:
- A statement does something but produces no value.
let x = 5;is a statement. - An expression evaluates to a value.
2 + 3is an expression — it becomes5.
Look again at the body of add:
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon! this is the return value
}
fn main() {
println!("{}", add(10, 20));
}
The last line is a + b with no semicolon. In Rust, the final expression in a function body — written without a semicolon — is what the function returns. There’s no return keyword needed. The compiler thinks: “the last thing here is a value, and the function promised to return a value, so that’s the answer.”
Now watch what a single semicolon does:
fn add(a: i32, b: i32) -> i32 {
a + b; // ERROR: the ; throws the value away
}
fn main() {
println!("{}", add(1, 2));
}
Press Run. The error says something like “mismatched types: expected i32, found ().” That () (called “unit”) means “nothing.” By adding ; you turned the expression a + b into a statement — you computed the sum and then threw it away. The function now returns nothing, but you promised an i32. A trailing semicolon is the number-one function bug in Rust.
You can use return too
The no-semicolon style is the idiomatic Rust way, but return also works and is required when you want to leave early:
fn describe(n: i32) -> &'static str {
if n < 0 {
return "negative"; // leave early
}
"zero or positive" // last expression, no semicolon
}
fn main() {
println!("{}", describe(-4));
println!("{}", describe(7));
}
Notice both styles appear here: an early return (with a semicolon, because it’s a statement) and the final expression without one. Both are hand back a value.
Functions that return nothing
If a function just does something (like printing) and has no answer to give, skip the arrow:
fn greet(name: &str) {
println!("Hello, {name}!"); // just does a thing; returns nothing
}
fn main() {
greet("Shaon");
greet("Rust");
}
&str is the type for a borrowed piece of text — you’ll see it constantly. For now just read it as “some text.”
Order doesn’t matter
Unlike some languages, you can call a function that’s defined below where you call it. Rust reads the whole file before deciding, so main can call add even if add is written afterward. Arrange your code however reads best.
Common mistakes
- The trailing semicolon on the return value.
fn f() -> i32 { x; }returns(), notx. The compiler says “expectedi32, found().” Remove the semicolon from the last line. This bites nearly everyone at first. - Forgetting parameter types.
fn add(a, b)won’t compile. Every parameter needs a type:fn add(a: i32, b: i32). Rust never infers these. - Forgetting the return type. If your function hands back a value, you must declare it with
-> Type. Without the arrow, Rust assumes the function returns nothing and complains when the body produces a value. - Mismatched return type. If you say
-> i32but the last expression is text, you get “mismatched types.” The declared type and the actual returned value must agree. - Adding
;after anif-expression you meant to return.fn f() -> i32 { if c { 1 } else { 2 }; }throws the value away. Drop the final;.
More examples
A guard clause for a quick exit
When one condition makes the rest of the function pointless, return immediately instead of nesting everything else inside an else.
fn discount_price(price: f64, is_member: bool) -> f64 {
if !is_member {
return price; // guard clause: no discount, leave early
}
price * 0.9
}
fn main() {
println!("{}", discount_price(100.0, false));
println!("{}", discount_price(100.0, true));
}
Taking a slice instead of a Vec
Writing the parameter as &[i32] lets the function accept an array, a Vec, or any borrowed chunk of one — it doesn’t care how the caller stored the data.
fn average(scores: &[i32]) -> f64 {
let sum: i32 = scores.iter().sum();
sum as f64 / scores.len() as f64
}
fn main() {
let quiz1 = [90, 85, 78];
let quiz2 = vec![100, 95];
println!("{:.1}", average(&quiz1)); // works on an array
println!("{:.1}", average(&quiz2)); // and on a Vec
}
Returning several values as a tuple
Finding both the smallest and largest value in one pass means the function has two answers to hand back — a tuple lets it return them together.
fn min_max(values: &[i32]) -> (i32, i32) {
let mut min = values[0];
let mut max = values[0];
for &v in values {
if v < min { min = v; }
if v > max { max = v; }
}
(min, max)
}
fn main() {
let (lo, hi) = min_max(&[4, 9, 1, 7]);
println!("low {lo}, high {hi}");
}
A recursive function
Factorial is naturally defined in terms of itself — 5! is 5 * 4! — so a function that calls itself is the most direct way to write it.
fn factorial(n: u64) -> u64 {
if n == 0 {
1
} else {
n * factorial(n - 1)
}
}
fn main() {
println!("5! = {}", factorial(5));
}
One function, reused everywhere the logic is needed
The moment you format a price in two places, you risk the two copies drifting apart. A function keeps the formatting rule in exactly one spot.
fn format_price(cents: u32) -> String {
format!("${}.{:02}", cents / 100, cents % 100)
}
fn main() {
let item = 1999;
let tax = 160;
println!("item: {}", format_price(item));
println!("tax: {}", format_price(tax));
println!("total: {}", format_price(item + tax));
}
Your turn
This function is meant to double a number and return it, but it doesn’t compile. Two things are wrong. Fix it. Press ▶ Run.
fn double(n) {
n * 2;
}
fn main() {
let result = double(21);
println!("double is {result}");
}
Show solution
The parameter needs a type, the function needs a return type, and the last line must lose its semicolon so the value is actually returned.
fn double(n: i32) -> i32 {
n * 2
}
fn main() {
let result = double(21);
println!("double is {result}"); // 42
}
n: i32 gives the input a type, -> i32 promises an answer, and removing the ; after n * 2 makes that the return value.
Quick check
Remember this
- Define a function with
fn name(params) -> ReturnType { body }. - Every parameter needs a type; Rust never guesses them.
- The last expression with no semicolon is the return value — no
returnkeyword needed. - Adding a
;to that last line throws the value away and returns()(nothing) — a very common error. - Use
returnto leave a function early; leave off-> Typewhen a function returns nothing.
Go deeper
- Rust Book - Functions — Basic function syntax.
Next: