Guide contents

Lesson 7 of 94

Functions

The key idea is that almost everything in Rust is an expression that produces a value, including blocks. That changes how you return values.

Concept Rust
Function with a return value fn add(a: i32, b: i32) -> i32 { a + b }
Function with no return value fn log(msg: &str)
Overloading Not supported: use different names
Default parameter values Not supported
Failure panic!(...) for bugs, Result for expected errors

1. Defining and calling a function

Rust
fn greet() {
    println!("Welcome to the store!");
}

fn main() {
    greet();
    greet();
}
Output
Welcome to the store!
Welcome to the store!

Functions use snake_case names. They can be defined in any order: main can call a function written below it.

2. Parameters must have types

Every parameter needs a type annotation. Rust never infers parameter types, because a function's signature is its contract with its callers.

Rust
fn print_order(order_id: u32, quantity: i32) {
    println!("Order #{order_id}: {quantity} item(s)");
}

fn main() {
    print_order(1001, 3);
    print_order(1002, 1);
}
Output
Order #1001: 3 item(s)
Order #1002: 1 item(s)

Leaving out a type is an error:

Rust✗ does not compile
fn double(x) {
    println!("{}", x * 2);
}

fn main() {
    double(4);
}
Output
error: expected one of `:`, `@`, or `|`, found `)`
 --> src/main.rs:1:12
  |
1 | fn double(x) {
  |            ^ expected one of `:`, `@`, or `|`
  |
help: if this is a parameter name, give it a type
  |
1 | fn double(x: TypeName) {
  |            ++++++++++
help: if this is a type, explicitly ignore the parameter name
  |
1 | fn double(_: x) {
  |           ++

3. Returning a value

Write the return type after ->. The last expression of the body, written without a semicolon, is the return value.

Rust
fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    let sum = add(19, 23);
    println!("19 + 23 = {sum}");
}
Output
19 + 23 = 42

4. The semicolon mistake

Adding a semicolon turns an expression into a statement, which produces no value. This is the most common error for newcomers, and the compiler explains it well.

Rust✗ does not compile · E0308
fn add(a: i32, b: i32) -> i32 {
    a + b;
}

fn main() {
    println!("{}", add(1, 2));
}
Output
error[E0308]: mismatched types
 --> src/main.rs:1:27
  |
1 | fn add(a: i32, b: i32) -> i32 {
  |    ---                    ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
2 |     a + b;
  |          - help: remove this semicolon to return this value

5. return for early exit

Idiomatic Rust uses return for early exits and lets the final expression be the normal result.

Rust
fn shipping_cost(order_total: f64) -> f64 {
    if order_total >= 50.0 {
        return 0.0;
    }
    4.99
}

fn main() {
    println!("{}", shipping_cost(75.0));
    println!("{}", shipping_cost(20.0));
}
Output
0
4.99

6. Blocks are expressions

A block { ... } evaluates to its last expression. You can use a block anywhere a value is expected, which lets you compute a value with temporary variables that do not leak out.

Rust
fn main() {
    let area = {
        let width = 12;
        let height = 5;
        width * height
    };
    println!("Area: {area}");
}
Output
Area: 60

7. if is an expression too

Because if produces a value, you can return it directly. This replaces the ternary operator ? : found in other languages.

Rust
fn describe(temperature: i32) -> &'static str {
    if temperature < 0 {
        "freezing"
    } else if temperature < 20 {
        "cold"
    } else {
        "warm"
    }
}

fn main() {
    println!("{}", describe(-5));
    println!("{}", describe(12));
    println!("{}", describe(28));
}
Output
freezing
cold
warm

The return type &'static str means "text that lives for the whole program", such as a string literal. Read it as "a string literal" for now. Lifetime Elision & 'static explains it fully.

8. Functions that return nothing return ()

A function without -> returns the unit type (), an empty value. It is a real value you can store and print.

Rust
fn log_event(event: &str) {
    println!("[event] {event}");
}

fn main() {
    let result = log_event("user_signed_in");
    println!("log_event returned {:?}", result);
}
Output
[event] user_signed_in
log_event returned ()

9. Parameters are copies for simple types

Numbers, booleans, and characters are copied into the function. Changing the parameter never changes the caller's variable. To change a parameter inside the function, mark it mut.

Rust
fn countdown_from(mut n: i32) {
    n -= 1;
    println!("inside: {n}");
}

fn main() {
    let start = 5;
    countdown_from(start);
    println!("caller still has: {start}");
}
Output
inside: 4
caller still has: 5

For non-simple types such as String, passing a value moves it instead of copying. That is the subject of the whole Ownership section.

10. No overloading

Rust does not allow two functions with the same name in the same scope.

Rust✗ does not compile · E0428
fn area(side: f64) -> f64 {
    side * side
}

fn area(width: f64, height: f64) -> f64 {
    width * height
}

fn main() {
    println!("{}", area(2.0));
}
Output
error[E0428]: the name `area` is defined multiple times
 --> src/main.rs:5:1
  |
1 | fn area(side: f64) -> f64 {
  | ------------------------- previous definition of the value `area` here
...
5 | fn area(width: f64, height: f64) -> f64 {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `area` redefined here
  |
  = note: `area` must be defined only once in the value namespace of this module

Use descriptive names instead. Later you will also see traits and generics, which cover most real uses of overloading.

Rust
fn square_area(side: f64) -> f64 {
    side * side
}

fn rectangle_area(width: f64, height: f64) -> f64 {
    width * height
}

fn main() {
    println!("{}", square_area(2.0));
    println!("{}", rectangle_area(2.0, 3.5));
}
Output
4
7

11. No default parameter values

Rust has no default arguments. The usual pattern is a short wrapper function that fills in the default. Later you will see the builder pattern and the Default trait for structs with many options.

Rust
fn connect_with_timeout(host: &str, timeout_secs: u32) {
    println!("connecting to {host} (timeout {timeout_secs}s)");
}

fn connect(host: &str) {
    connect_with_timeout(host, 30);
}

fn main() {
    connect("db.internal");
    connect_with_timeout("cache.internal", 5);
}
Output
connecting to db.internal (timeout 30s)
connecting to cache.internal (timeout 5s)

12. Functions calling functions

Small functions that each do one thing combine well.

Rust
fn celsius_to_fahrenheit(c: f64) -> f64 {
    c * 9.0 / 5.0 + 32.0
}

fn fahrenheit_to_celsius(f: f64) -> f64 {
    (f - 32.0) * 5.0 / 9.0
}

fn round_to_one_decimal(x: f64) -> f64 {
    (x * 10.0).round() / 10.0
}

fn main() {
    let boiling = celsius_to_fahrenheit(100.0);
    let body = fahrenheit_to_celsius(98.6);
    println!("100°C = {}°F", boiling);
    println!("98.6°F = {}°C", round_to_one_decimal(body));
}
Output
100°C = 212°F
98.6°F = 37°C

13. Returning several values with a tuple

A function can return several values at once in a tuple, written (a, b). There is no need to create a class just to hold two results. Tuples & Arrays covers tuples in detail.

Rust
fn divide_with_remainder(dividend: i32, divisor: i32) -> (i32, i32) {
    (dividend / divisor, dividend % divisor)
}

fn main() {
    let (hours, minutes) = divide_with_remainder(135, 60);
    println!("135 minutes = {hours}h {minutes}m");
}
Output
135 minutes = 2h 15m

14. Recursion

Rust
fn factorial(n: u64) -> u64 {
    if n == 0 { 1 } else { n * factorial(n - 1) }
}

fn main() {
    println!("5! = {}", factorial(5));
    println!("20! = {}", factorial(20));
}
Output
5! = 120
20! = 2432902008176640000

20! is the largest factorial that fits in a u64. 21! would overflow and panic in a debug build. Rust does not guarantee tail-call optimization, so very deep recursion can overflow the stack. Prefer loops for deep iteration.

15. Functions inside functions

You can define a helper function inside another function. It is only visible there, which keeps it out of the rest of the file.

Rust
fn format_duration(total_seconds: u32) -> String {
    fn pad(n: u32) -> String {
        format!("{n:02}")
    }
    let hours = total_seconds / 3600;
    let minutes = total_seconds % 3600 / 60;
    let seconds = total_seconds % 60;
    format!("{}:{}:{}", pad(hours), pad(minutes), pad(seconds))
}

fn main() {
    println!("{}", format_duration(3725));
    println!("{}", format_duration(59));
}
Output
01:02:05
00:00:59

The return type String is owned, growable text. It is introduced in A First Look at String, Vec & HashMap.

16. panic! for bugs

panic! stops the program with an error message. Use it for situations that should be impossible if the code is correct, such as a failed assert. Expected failures such as bad user input use Result, covered in Result & the ? Operator.

Rustpanics at runtime
fn percentage(part: u32, whole: u32) -> u32 {
    if whole == 0 {
        panic!("percentage() called with whole = 0");
    }
    part * 100 / whole
}

fn main() {
    println!("{}%", percentage(1, 4));
    println!("{}%", percentage(1, 0));
}
Output
25%

thread 'main' panicked at src/main.rs:3:9:
percentage() called with whole = 0

The badge panics at runtime marks examples that deliberately end in a panic. The output shows what was printed before the panic, then the panic message with the file, line, and column.

17. Runtime integer overflow panics in debug builds

The previous lesson showed that the compiler rejects overflow it can see. When the values only arrive at runtime, a debug build panics instead:

Rustpanics at runtime
fn add_stock(current: u8, received: u8) -> u8 {
    current + received
}

fn main() {
    println!("{}", add_stock(100, 50));
    println!("{}", add_stock(250, 10));
}
Output
150

thread 'main' panicked at src/main.rs:2:5:
attempt to add with overflow

In a release build (cargo run --release) the second call would silently return 4. That is why production code that could overflow should call checked_add, saturating_add, or wrapping_add explicitly.

18. Functions that never return: !

A function whose return type is ! (the "never" type) never returns normally: it always panics, loops forever, or exits the process. The compiler uses this knowledge, for example so you can call it where any type of value is expected.

Rustpanics at runtime
fn fail(message: &str) -> ! {
    panic!("fatal: {message}");
}

fn port_from_config(configured: i32) -> u16 {
    if configured > 0 && configured <= 65535 {
        configured as u16
    } else {
        fail("port out of range")
    }
}

fn main() {
    println!("port {}", port_from_config(8080));
    println!("port {}", port_from_config(70000));
}
Output
port 8080

thread 'main' panicked at src/main.rs:2:5:
fatal: port out of range

19. const fn: functions the compiler can run

A const fn can be evaluated at compile time, so it can initialize a const. The result is computed during compilation and costs nothing at runtime.

Rust
const fn kilobytes(n: u64) -> u64 {
    n * 1024
}

const fn megabytes(n: u64) -> u64 {
    kilobytes(n) * 1024
}

const MAX_UPLOAD_BYTES: u64 = megabytes(25);

fn main() {
    println!("Max upload: {MAX_UPLOAD_BYTES} bytes");
    println!("At runtime too: {}", kilobytes(4));
}
Output
Max upload: 26214400 bytes
At runtime too: 4096

20. Comments and documentation comments

// is a line comment and /* */ is a block comment. /// is a documentation comment. It is written in Markdown, and cargo doc turns it into HTML.

Rust
/// Returns the price after applying a percentage discount.
///
/// `percent` must be between 0 and 100.
fn discounted(price: f64, percent: f64) -> f64 {
    // Convert the percentage to a multiplier, e.g. 15.0 -> 0.85
    let multiplier = 1.0 - percent / 100.0;
    /* A block comment works too. */
    price * multiplier
}

fn main() {
    println!("{:.2}", discounted(80.0, 15.0));
}
Output
68.00

21. Realistic example: a loan payment calculator

This implements the standard amortization formula as a pure function.

Rust
/// Monthly payment for a fixed-rate loan.
fn monthly_payment(principal: f64, annual_rate_percent: f64, years: u32) -> f64 {
    let monthly_rate = annual_rate_percent / 100.0 / 12.0;
    let months = (years * 12) as i32;
    if monthly_rate == 0.0 {
        return principal / months as f64;
    }
    let factor = (1.0 + monthly_rate).powi(months);
    principal * monthly_rate * factor / (factor - 1.0)
}

fn main() {
    let payment = monthly_payment(250_000.0, 4.5, 30);
    let total_paid = payment * 360.0;
    println!("Monthly payment: {payment:.2}");
    println!("Total paid:      {total_paid:.2}");
    println!("Total interest:  {:.2}", total_paid - 250_000.0);
    println!("Zero interest:   {:.2}", monthly_payment(12_000.0, 0.0, 1));
}
Output
Monthly payment: 1266.71
Total paid:      456016.78
Total interest:  206016.78
Zero interest:   1000.00

22. Realistic example: validating a password

Small bool-returning functions make rules easy to read and test.

Rust
const MIN_LENGTH: usize = 12;

fn is_long_enough(password: &str) -> bool {
    password.len() >= MIN_LENGTH
}

fn has_digit(password: &str) -> bool {
    password.chars().any(|c| c.is_ascii_digit())
}

fn has_uppercase(password: &str) -> bool {
    password.chars().any(|c| c.is_uppercase())
}

fn is_strong(password: &str) -> bool {
    is_long_enough(password) && has_digit(password) && has_uppercase(password)
}

fn main() {
    println!("{}", is_strong("hunter2"));
    println!("{}", is_strong("correcthorsebattery"));
    println!("{}", is_strong("Correct-Horse-Battery-9"));
}
Output
false
false
true

password.chars().any(|c| c.is_ascii_digit()) checks whether any character is a digit. It uses an iterator and a closure (Rust's lambda syntax, |c| ...). Both get full lessons later: Closures and Iterators.

23. Realistic example: tiered shipping rules

Early returns handle special cases first, and the last expression handles the general case.

Rust
fn shipping_cents(weight_grams: u32, is_express: bool, is_member: bool) -> u32 {
    if weight_grams == 0 {
        return 0;
    }
    if is_member && !is_express {
        return 0;
    }
    let base = if weight_grams <= 500 {
        499
    } else if weight_grams <= 2_000 {
        899
    } else {
        899 + (weight_grams - 2_000) / 1_000 * 250
    };
    if is_express { base * 2 } else { base }
}

fn main() {
    println!("{}", shipping_cents(300, false, false));
    println!("{}", shipping_cents(1_500, true, false));
    println!("{}", shipping_cents(5_500, false, false));
    println!("{}", shipping_cents(5_500, false, true));
    println!("{}", shipping_cents(5_500, true, true));
}
Output
499
1798
1649
0
3298

24. Realistic example: a leap year and days-in-month helper

Rust
fn is_leap_year(year: u32) -> bool {
    (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}

fn days_in_month(year: u32, month: u32) -> u32 {
    if month == 2 {
        if is_leap_year(year) { 29 } else { 28 }
    } else if month == 4 || month == 6 || month == 9 || month == 11 {
        30
    } else {
        31
    }
}

fn main() {
    println!("Feb 2024: {}", days_in_month(2024, 2));
    println!("Feb 2100: {}", days_in_month(2100, 2));
    println!("Feb 2000: {}", days_in_month(2000, 2));
    println!("Sep 2026: {}", days_in_month(2026, 9));
}
Output
Feb 2024: 29
Feb 2100: 28
Feb 2000: 29
Sep 2026: 30

Summary

  • fn name(param: Type) -> ReturnType { ... }. Parameter types are always required.
  • The last expression without a semicolon is the return value. return is for early exits.
  • Blocks and if are expressions that produce values.
  • A function with no -> returns (), the unit type.
  • No overloading and no default arguments: use distinct names and small wrapper functions.
  • panic! is for bugs. -> ! marks a function that never returns. const fn can run at compile time.
  • /// writes documentation.

Next: Control Flow.