Functions & Expressions

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. Rust is an expression-oriented language: almost every construct produces a value, and the difference between an expression and a statement is often a single semicolon. This page explains how that works for functions and blocks; control-flow expressions (if, match, loop) have their own page, Control Flow.

Defining and calling functions

Functions are declared with fn, a snake_case name, a parameter list, and an optional return type after ->. Every parameter must have a type annotation — function signatures are the boundary where Rust’s type inference stops, which keeps error messages local and makes signatures readable documentation. Functions can be declared in any order; a call before the definition is fine.

fn main() {
    let fee = late_fee(3, 50);
    print_receipt("Dune", fee);
}

/// Late fee in cents: `cents_per_day` for each day late, capped at ten days.
fn late_fee(days_late: u32, cents_per_day: u32) -> u32 {
    days_late.min(10) * cents_per_day
}

fn print_receipt(title: &str, fee_cents: u32) {
    println!("{title}: {}.{:02}", fee_cents / 100, fee_cents % 100);
}

Arguments are passed by value: the argument is moved (or copied, for Copy types) into the parameter. To let a function use a value without taking it, pass a reference (&T or &mut T); the ownership consequences are covered in Ownership & Moves. Rust has no default arguments, no named arguments and no overloading by parameter types; the usual alternatives are distinct function names (with_capacity), builder types, Option parameters, or traits.

Calling a function with the wrong number of arguments is error E0061; passing the wrong type is E0308.

Patterns in parameters

A parameter is an irrefutable pattern, so tuples and structs can be destructured directly in the signature:

struct Point {
    x: i32,
    y: i32,
}

fn distance_squared(&(x1, y1): &(i32, i32), Point { x: x2, y: y2 }: Point) -> i32 {
    (x2 - x1).pow(2) + (y2 - y1).pow(2)
}

fn main() {
    println!("{}", distance_squared(&(0, 0), Point { x: 3, y: 4 })); // 25
}

Statements and expressions

  • An expression evaluates to a value: 5, a + b, late_fee(3, 50), a block { ... }, if, match, a macro call such as format!().

  • A statement performs an action and has no value. Rust has two kinds: declaration statements (let and items) and expression statements (an expression followed by ;).

Because let is a statement, let a = (let b = 6); does not compile, and there is no a = b = 6 chaining. An assignment expression such as x = 6 does exist, but it evaluates to ().

Blocks are expressions

A block { ... } is an expression whose value is its last expression — the tail expression — provided that expression is not followed by a semicolon. Adding a semicolon turns it into a statement and the block evaluates to ():

fn main() {
    let copies = 4;
    let label = {
        let plural = if copies == 1 { "" } else { "s" };
        format!("{copies} cop{}", if plural.is_empty() { "y" } else { "ies" }) // tail: no semicolon
    };
    println!("{label}"); // 4 copies

    let total = {
        let extra = 1;      // statement
        copies + extra      // tail expression: the block evaluates to 5
    };
    println!("{total}");
    // With `copies + extra;` (note the semicolon) the block would evaluate to `()`.
}

Return values and the tail expression

A function returns the value of its body block — that is, its tail expression. The return keyword exits early. Idiomatic Rust uses the tail expression for the normal result and return only for early exits:

fn loan_days(member_kind: &str, renewals: u8) -> u16 {
    if renewals >= 3 {
        return 0; // early exit: no more renewals
    }
    let base = match member_kind {
        "student" => 28,
        "staff" => 42,
        _ => 21,
    };
    base + u16::from(renewals) * 7 // tail expression: the return value
}

fn main() {
    println!("{} {}", loan_days("student", 1), loan_days("guest", 3)); // 35 0
}

The most common mistake is a stray semicolon after the tail expression. The function body then evaluates to () and the compiler reports a type mismatch, with a helpful hint:

fn add_tax(cents: u32) -> u32 {
    cents * 121 / 100; // error[E0308]: mismatched types -- expected `u32`, found `()`
}

fn main() {
    println!("{}", add_tax(1000));
}
# error[E0308]: mismatched types
#  --> src/main.rs:1:27
#   |
# 1 | fn add_tax(cents: u32) -> u32 {
#   |    -------                ^^^ expected `u32`, found `()`
#   |    |
#   |    implicitly returns `()` as its body has no tail or `return` expression
# 2 |     cents * 121 / 100;
#   |                      - help: remove this semicolon to return this value

Source: Error E0308

A function with no -> returns (). main may also return Result<(), E> or std::process::ExitCode; see Error Handling.

Diverging functions

A function whose return type is ! (the never type) never returns normally: it panics, loops forever, or exits the process. Calls to it can stand in for a value of any type, which the compiler uses for flow analysis:

fn usage_error(message: &str) -> ! {
    eprintln!("usage error: {message}");
    std::process::exit(2)
}

fn parse_count(arg: Option<String>) -> u32 {
    let Some(text) = arg else { usage_error("missing count") };
    text.parse().unwrap_or_else(|_| usage_error("count must be a number"))
}

fn main() {
    let count = parse_count(Some("3".to_string()));
    println!("{count}");
}

panic!, unreachable!, todo!, unimplemented! and std::process::exit all have type !.

const fn

A const fn can be called in constant contexts — const and static initialisers, array lengths, const generic arguments, const {} blocks — and is evaluated by the compiler there. Called at run time, it behaves like an ordinary function. The set of operations allowed in a const fn has grown steadily: loops, if/match, mutable references (1.83), floating-point arithmetic (1.82) and many standard-library methods are allowed; heap allocation, trait method calls through generics and I/O are not.

const fn isbn_check_digit(first12: [u8; 12]) -> u8 {
    let mut sum = 0u32;
    let mut i = 0;
    while i < 12 {
        // `for` loops are not yet allowed in const fn; `while` is.
        sum += first12[i] as u32 * if i % 2 == 0 { 1 } else { 3 };
        i += 1;
    }
    ((10 - sum % 10) % 10) as u8
}

const DUNE_CHECK: u8 = isbn_check_digit([9, 7, 8, 0, 4, 4, 1, 0, 1, 3, 5, 9]); // computed at compile time

fn main() {
    println!("{DUNE_CHECK}"); // 3
    let runtime = isbn_check_digit([9, 7, 8, 1, 7, 1, 8, 5, 0, 3, 1, 0]); // also callable at run time
    println!("{runtime}"); // 6
}

Making a public function const is a promise: removing const later is a breaking change, so library authors add it deliberately.

Function items and function pointers

Each function has its own unique, zero-sized function item type, which the compiler prints as fn(u32, u32) -> u32 {late_fee}. Function items coerce to function pointer types written fn(u32, u32) -> u32, a plain code address (pointer-sized) that can be stored in variables, collections and struct fields:

fn by_title(a: &(&str, u16), b: &(&str, u16)) -> std::cmp::Ordering {
    a.0.cmp(b.0)
}

fn by_year(a: &(&str, u16), b: &(&str, u16)) -> std::cmp::Ordering {
    a.1.cmp(&b.1)
}

type Comparator = fn(&(&str, u16), &(&str, u16)) -> std::cmp::Ordering;

fn main() {
    let item = by_title;                       // function item: zero-sized, unique type
    let pointer: Comparator = by_year;         // coerced to a function pointer
    println!("{} {}", size_of_val(&item), size_of_val(&pointer)); // 0 8

    let orders: [(&str, Comparator); 2] = [("title", by_title), ("year", by_year)];
    let mut books = vec![("Emma", 1815), ("Dune", 1965), ("Beloved", 1987)];
    for (name, cmp) in orders {
        books.sort_by(cmp);
        println!("by {name}: {books:?}");
    }
}

Function items are zero-sized and calls through them are statically dispatched and inlinable; function pointers cost an indirect call. Closures that capture nothing also coerce to function pointers. Closures that do capture variables cannot — they implement the Fn traits instead, and generic code usually accepts impl Fn(...) so that both functions and closures work; see Closures & Fn Traits.

Nested functions

Functions (and other items) may be declared inside a function body. They are visible only within that block, but they are ordinary items: they cannot capture the enclosing function’s local variables (use a closure for that).

fn summarise(titles: &[&str]) -> String {
    fn initial(title: &str) -> char {
        title.chars().next().unwrap_or('?')
    }
    titles.iter().map(|t| initial(t)).collect()
}

fn main() {
    println!("{}", summarise(&["Dune", "Emma", "Ulysses"])); // DEU
}

Methods and associated functions: a preview

Functions defined inside an impl block belong to a type. Those whose first parameter is self, &self or &mut self are methods, called with dot syntax; the others are associated functions, called with :: and typically used as constructors:

struct Shelf {
    titles: Vec<String>,
}

impl Shelf {
    fn new() -> Self {                       // associated function: Shelf::new()
        Self { titles: Vec::new() }
    }

    fn add(&mut self, title: &str) {         // method: shelf.add(...)
        self.titles.push(title.to_owned());
    }

    fn len(&self) -> usize {
        self.titles.len()
    }
}

fn main() {
    let mut shelf = Shelf::new();
    shelf.add("Dune");
    println!("{}", shelf.len());
    println!("{}", Shelf::len(&shelf));      // method called as a function: same thing
}

Receivers, constructors and Default are covered in Structs & Methods.