Functions in Rust
Functions are fundamental to Rust. You have already seen one in every example — the main function. If you have worked with C or C++, you will recognise it: main is the entry point of every Rust program.
Functions in Rust are declared using the fn keyword. Rust uses snake_case as the conventional style for both function names and variable names.
Anatomy of a Rust Function
Naming Rules
Rust enforces specific rules for function names. The table below shows what is allowed and what is not:
| Rule | Valid Example | Invalid Example |
|---|---|---|
Must start with a letter (a–z, A–Z) or underscore _ | add, _hidden | 2add, -start |
| Can contain letters, digits, and underscores | calculate2_sum, _helper123 | add-sum, sum!, my func |
| Cannot be a Rust keyword | process_data | fn, let, struct |
| Should use snake_case (idiomatic Rust) | calculate_sum, print_message | calculateSum, CalculateSum |
| Leading underscore suppresses unused-variable warnings | _internal_helper | — |
| No spaces or special symbols | sum_numbers | sum numbers, sum$ |
The Rust compiler will warn you if a function name uses camelCase instead of snake_case. While the program still compiles, following the convention keeps your code consistent with the rest of the ecosystem.
Defining and Calling Functions
A function is declared with the fn keyword, followed by the function name, a pair of parentheses, and a body enclosed in curly braces. Rust does not require functions to be declared before they are used — the compiler will find them anywhere within the same scope.
fn main() {
println!("Hello, world!");
greet();
}
fn greet() {
println!("Hello from a function.");
}cargo run
Compiling rust_app v0.1.0
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.91s
Running `target/debug/rust_app`
Hello, world!
Hello from a function.The output appears in the order the calls appear in main. println!("Hello, world!") runs first, then greet() runs next.
Parameters
A parameter is a variable in the function’s definition. When you call the function and supply a value, that value is called an argument. Technically the two words are distinct: parameters live in the definition, arguments live at the call site.
fn add(a: i32, b: i32) -> i32 { // a and b are parameters
a + b
}
fn main() {
let result = add(5, 3); // 5 and 3 are arguments
println!("Sum: {}", result);
}Sum: 8Type Annotations Are Required
In a function signature, you must annotate the type of every parameter. This is a deliberate design choice: because types are explicit in function boundaries, the compiler never has to guess and can catch mistakes early with precise error messages.
fn main() {
print_price(99.99, "Dollar");
}
fn print_price(price: f64, currency: &str) {
println!("Price: {price} {currency}");
}Price: 99.99 Dollar| Parameter | Type | Meaning |
|---|---|---|
price | f64 | 64-bit floating-point number |
currency | &str | A string slice — a borrowed reference to text |
Statements and Expressions
Every line inside a Rust function is either a statement or an expression. Rust keeps this distinction strict — understanding it is key to writing correct functions.
| Statement | Expression | |
|---|---|---|
| Produces a value? | No | Yes |
Ends with ;? | Yes | No (adding ; turns it into a statement) |
| Can be assigned to a variable? | No | Yes |
| Example | let x = 5; | 5 + 3, { price + shipping } |
You Cannot Assign a Statement to a Variable
Because statements do not return a value, you cannot assign one to another variable:
fn main() {
let x = (let apples = 5); // error!
}error: expected expression, found `let` statement
--> src/main.rs:2:14
|
2 | let x = (let apples = 5);
| ^^^
|
= note: only supported directly in conditions of `if` and `while` expressionsIn languages like C or Ruby, x = y = 6 works because assignment returns a value. In Rust, let is a statement — there is nothing for x to hold.
Blocks Are Expressions
A block — code enclosed in { } — is itself an expression. Its value is whatever the last line inside it evaluates to (without a semicolon):
fn main() {
let total = {
let price = 100;
let shipping = 20;
price + shipping // no semicolon → this is the value of the block
};
println!("Total: {total}"); // Total: 120
}| Line | Type | Note |
|---|---|---|
let price = 100; | Statement | Binds 100 to price |
let shipping = 20; | Statement | Binds 20 to shipping |
price + shipping | Expression | Evaluates to 120 — the block’s return value |
let total = { ... }; | Statement | Binds 120 to total |
If you add a semicolon after price + shipping, it becomes a statement. The block then returns () (the unit type), and the compiler will raise a type mismatch error.
Return Values
A Rust function returns the value of its final expression. You declare the expected return type using -> in the function signature. No return keyword is needed for the common case.
fn five() -> i32 {
5 // no semicolon — this is the return value
}
fn main() {
let x = five(); // x = 5
println!("{x}");
}The -> i32 in the signature is a promise: “this function will hand back a 32-bit integer.” The bare 5 at the end fulfils that promise.
Implicit vs Explicit Return
| Style | When to use | Example |
|---|---|---|
| Implicit (last expression) | Normal case — the function completes naturally | x + 1 as the final line |
Explicit (return) | Early exit — bail out before reaching the end | return n; inside an if block |
fn plus_one(x: i32) -> i32 {
x + 1 // implicit return — no semicolon
}
fn absolute(n: i32) -> i32 {
if n >= 0 {
return n; // explicit early return
}
-n // implicit return for the negative case
}The Semicolon Trap
Adding a semicolon to the last line turns an expression into a statement, which changes what the function returns:
fn plus_one(x: i32) -> i32 {
x + 1; // semicolon → statement → returns (), not i32
}error[E0308]: mismatched types
--> src/main.rs:2:24
|
1 | fn plus_one(x: i32) -> i32 {
| ^^^ expected `i32`, found `()`The compiler even suggests the fix: remove the semicolon to return the value. This is one of the most common beginner mistakes in Rust.
Summary
| Concept | Key Rule |
|---|---|
| Declaration | Use fn, followed by name, parameters, optional -> return type, and { body } |
| Naming | Use snake_case for function and variable names |
| Parameters | Every parameter must have an explicit type annotation in the signature |
| Statement | Performs an action, returns nothing, ends with ; |
| Expression | Evaluates to a value, has no trailing ; |
| Implicit return | The last expression in the function body is the return value |
| Explicit return | Use the return keyword to exit a function early |
| Semicolon trap | Adding ; to the last line makes it return () instead of the intended type |
What’s Next?
Now that you understand how functions work in Rust, you are ready to explore Control Flow — where you will learn how to use if expressions, loops, and pattern matching to control the execution of your programs.