Functions
Write Rust functions with typed parameters and return types, understand the expression-based return style, and return multiple values using tuples.
Introduction
You've already written and read several Rust functions in earlier lessons. This lesson slows down and covers the details: how parameters and return types are declared, the crucial difference between expressions and statements, and how to return more than one value.
These details matter more in Rust than in many languages, because Rust's "no semicolon means return this" convention trips up almost every newcomer at least once.
- How to declare functions, parameters, and return types.
- The difference between an expression and a statement, and why it matters for return values.
- How to return multiple values from a function using a tuple.
- How passing references into functions relates to the ownership rules from earlier.
Defining Functions
Functions are declared with `fn`, followed by a name, parentheses containing parameters, and an optional `-> ReturnType`. Unlike `let` bindings, function parameters always require an explicit type annotation — Rust does not infer parameter types from how a function is called.
Expressions vs Statements
In Rust, a statement performs an action but does not produce a value (and always ends in a semicolon); an expression evaluates to a value. The body of a function is a series of statements optionally ending in an expression, and if the last line has no trailing semicolon, its value automatically becomes the function's return value — no `return` keyword required.
This is exactly why an accidental semicolon is such a common beginner mistake: adding one to what was meant to be the return expression turns it into a statement, which evaluates to `()` (the "unit" type, Rust's equivalent of "nothing"), producing a type mismatch against the function's declared return type.
Returning Multiple Values
Rust functions can only return one value, but that value can be a tuple, which effectively lets you return several related pieces of data at once. The caller can then destructure the tuple directly into separate named variables.
Parameters and References
Following the ownership rules from earlier, passing a parameter by value moves or copies it into the function, while passing a reference (`&T` or `&mut T`) borrows it instead. Accepting `&str` rather than `String` in a parameter is common practice, since it lets a function read text without forcing the caller to give up ownership of their data.
Code Example
fn main() { let sum = add(5, 3); println!("Sum: {}", sum);
let (quotient, remainder) = divide(17, 5); println!("17 / 5 = {} remainder {}", quotient, remainder);
print_greeting("Rustacean");}
fn add(a: i32, b: i32) -> i32 { a + b // no semicolon: this is the return value}
fn divide(dividend: i32, divisor: i32) -> (i32, i32) { (dividend / divisor, dividend % divisor)}
fn print_greeting(name: &str) { println!("Hello, {}!", name);}Click Run to see what this code prints.
Common Mistakes
- Accidentally adding a semicolon after the final expression in a function, turning it into a statement that returns () and causing a "mismatched types" compile error.
- Forgetting that function parameters need explicit type annotations — Rust does not infer them the way it does for let bindings.
- Passing a String where a function expects a &str (or the reverse) without understanding what's being borrowed versus owned.
- Writing deeply nested logic inside one large function instead of extracting small, well-named helper functions.
Best Practices
- Let the final expression (without a semicolon) be your return value instead of an explicit `return`, where it reads naturally.
- Accept `&str` instead of `String` in function parameters when you only need to read the text, so callers aren't forced to give up ownership.
- Keep functions short and named after what they compute, not how they compute it.
- Use tuples for a couple of related return values, and reach for a struct once you have three or more.
Frequently Asked Questions
No — the idiomatic style is to omit the final semicolon and let the last expression be the return value; return is mainly used for early returns.
Not directly like some languages; common patterns instead include Option<T> parameters, builder-style structs, or multiple similarly-named functions.
Functions are declared with fn and cannot capture variables from their surrounding scope; closures can capture their environment and are covered in more advanced material.
Yes — functions with no explicit return type implicitly return the unit type (), Rust's equivalent of "no meaningful value."
Key Takeaways
- Function parameters always require explicit type annotations; the return type follows ->.
- A function's last expression, without a trailing semicolon, becomes its return value automatically.
- Adding a semicolon turns an expression into a statement that evaluates to (), a common source of type-mismatch errors.
- Tuples let a function return multiple related values at once.
Summary
You now understand Rust's function syntax in depth, including the expression-based return style that shows up constantly in idiomatic Rust code. Next, you'll work with Rust's two essential collection types — the growable Vec<T> and the key-value HashMap<K, V>.