Rust · Beginner

Functions in Rust

8 min readUpdated September 24, 2026Every example verified

In short: A Rust function is declared with fn name(param: Type) -> ReturnType { body }. Parameter types are mandatory, the value of the last expression in the body (written without a semicolon) is returned, and return exits early.

What a function is in Rust

A function packages a computation behind a name so it can be called from several places and understood on its own. In Rust the declaration carries everything a caller needs to know: fn sheets_needed(pages: u32, per_sheet: u32) -> u32. Every parameter has a type, written after a colon, and the return type follows an arrow. The compiler does not infer parameter types from the calls, and that is deliberate: the signature is a contract that can be read, checked and relied on without looking at the body or at any caller. A function that returns nothing omits the arrow; its return type is then the empty tuple (), which is what println! evaluates to as well.

The body is a block, and a block's value is its final expression. So fn double(x: i32) -> i32 { x * 2 } returns x * 2 because that expression has no semicolon. Adding a semicolon would turn it into a statement and the block's value into (), which the compiler rejects as a type mismatch. This is the single rule to internalise: statements end with a semicolon and produce nothing; the last expression without one is the result. The return keyword still exists for leaving early from inside a loop or a condition, and return value; at the end of a body is legal but unidiomatic.

Because if, match and blocks are expressions too, a body often reads as one expression: if pages % per_sheet == 0 { full } else { full + 1 } is a complete return value with no temporary variable. The control flow lesson introduced this; functions are where it pays off.

Calls must match the signature exactly. Passing a float where an i32 is expected is an error, not a conversion. There are no default arguments and no overloading: one name, one parameter list. To return several values, return a tuple and destructure it at the call site: let (hours, minutes) = split_minutes(135);. Functions can be declared anywhere in the file, before or after main, and the order does not matter.

A detail that trips up newcomers is text parameters. A function that only reads a string should take &str, the borrowed string slice. Callers can pass a literal directly, or &name when they hold a String; Rust converts &String to &str automatically. Taking String instead would force every caller to hand over ownership, which the ownership lesson explains. format! is the sibling of println! that builds a String instead of printing, and it is how a function returns text it has composed.

Syntax

 Rust · syntax
fn name(param: Type, other: Type) -> ReturnType {
    let local = param + other;    // a statement: ends with ;
    local * 2                     // the last expression is the return value
}

fn describe(label: &str) {        // no arrow: returns ()
    println!("label: {}", label);
}

fn split_minutes(total: u32) -> (u32, u32) {
    (total / 60, total % 60)      // two values returned as a tuple
}

fn discount(total: u32) -> u32 {
    if total < 50 {
        return 0;                 // early exit
    }
    total / 10
}

Names use snake_case. Every parameter needs a type; the return type is written after -> and is () when omitted.

Defining and calling functions

One function returns a value; the other returns nothing and prints instead.

 Rust
fn pallet_weight(crates: u32, kg_each: f64) -> f64 {
    crates as f64 * kg_each
}

fn print_label(customer: &str, kg: f64) {
    println!("{}: {:.1} kg", customer, kg);
}

fn main() {
    let first = pallet_weight(8, 2.25);
    print_label("Millbrook Print", first);
    print_label("Quay Cafe", pallet_weight(3, 4.0));
}

Output

Millbrook Print: 18.0 kg
Quay Cafe: 12.0 kg

pallet_weight takes two typed parameters and returns an f64; its body is a single expression, so there is no semicolon and no return. print_label has no arrow, returns (), and exists for its printed side effect. The second call to print_label passes the result of pallet_weight directly as an argument, which works because the value has exactly the type the parameter expects. Both functions are defined above main, but they could equally be placed after it.

Early return and returning a pair

Three cent amounts arrive on input; each is passed through discount_percent, and split_minutes returns two values at once.

 Rust
use std::io::BufRead;

fn discount_percent(total_cents: u32) -> u32 {
    if total_cents < 5000 {
        return 0;
    }
    if total_cents < 20000 {
        return 5;
    }
    10
}

fn split_minutes(total: u32) -> (u32, u32) {
    (total / 60, total % 60)
}

fn main() {
    for line in std::io::stdin().lock().lines() {
        let cents: u32 = line.unwrap().trim().parse().unwrap();
        println!("{} cents -> {}% off", cents, discount_percent(cents));
    }
    let (hours, minutes) = split_minutes(135);
    println!("Print run: {} h {} min", hours, minutes);
}

Input given to the program: 4200500025000

Output

4200 cents -> 0% off
5000 cents -> 5% off
25000 cents -> 10% off
Print run: 2 h 15 min

discount_percent uses return twice to leave as soon as a tier is decided, and its last line is the bare expression 10 for everything else. The early returns and the final expression all produce a u32, which the signature promises. split_minutes bundles hours and minutes into a tuple; the caller destructures it with let (hours, minutes) = ... so both values get names. The loop reads the input line by line, exactly as in the control flow lesson, and calls the function for each.

Expressions all the way down, and a helper that reads input

The input lines are 14 and 4. Notice that read_line, sheets_needed and the block assigned to note each end in an expression without a semicolon.

 Rust
use std::io::BufRead;

fn read_line() -> String {
    std::io::stdin().lock().lines().next().unwrap().unwrap()
}

fn sheets_needed(pages: u32, per_sheet: u32) -> u32 {
    let full = pages / per_sheet;
    if pages % per_sheet == 0 { full } else { full + 1 }
}

fn main() {
    let pages: u32 = read_line().trim().parse().unwrap();
    let per_sheet: u32 = read_line().trim().parse().unwrap();
    let sheets = sheets_needed(pages, per_sheet);
    let note = {
        let spare = sheets * per_sheet - pages;
        if spare == 0 { String::from("no blank pages") } else { format!("{} blank page(s)", spare) }
    };
    println!("{} sheets, {}", sheets, note);
}

Input given to the program: 144

Output

4 sheets, 2 blank page(s)

read_line wraps the noisy input idiom in a one-line function that returns a String; each call takes the next line of standard input. sheets_needed computes with a let statement and then returns an if expression. In main, note is assigned from a block: spare is local to it, and the if inside chooses between two String values, so both branches have the same type. format! builds text the way println! prints it. Fourteen pages at four per sheet need four sheets, with two pages left blank.

Common mistakes

  • Ending the return expression with a semicolon

    Why it goes wrong: fn double(x: i32) -> i32 { x * 2; } fails with E0308, mismatched types: the semicolon turns x * 2 into a statement, the block's value becomes (), and the signature promised i32. The compiler's help text says exactly which semicolon to remove.

    Fix: Leave the final expression bare, or write return x * 2;.

     Rust · fix
    fn double(x: i32) -> i32 {
        x * 2
    }
  • Leaving the type off a parameter

    Why it goes wrong: fn add(a, b) is a syntax error: Rust never infers parameter types, because the signature must stand on its own as documentation and as the thing the type checker trusts.

    Fix: Annotate every parameter: fn add(a: i32, b: i32) -> i32.

  • Passing a String to a function that takes &str

    Why it goes wrong: word_count(name) with name: String is a type mismatch (E0308). A String is not a &str, though a reference to one converts automatically.

    Fix: Pass a reference: word_count(&name). The compiler's help text suggests the borrow.

     Rust · fix
    fn word_count(text: &str) -> usize {
        text.split_whitespace().count()
    }
    
    let name = String::from("quay cafe");
    println!("{}", word_count(&name));
  • Calling a function with an argument of the wrong numeric type

    Why it goes wrong: pallet_weight(8.0, 2.25) fails when the first parameter is u32; Rust does not convert 8.0 to an integer or 8 to a float for you.

    Fix: Pass a value of the declared type, converting at the call site with as if you must.

Statements and expressions

CodeKindValue
let x = 5;statementnone
x + 1expressionthe sum
x + 1;expression statement()
{ let y = 2; y * x }block expressiony * x
if a { 1 } else { 2 }expression1 or 2
println!("hi")expression()

Where you use this

Most exercises on this site want the same three things: read input, compute, print. Putting the computation into a function with a clear signature such as fn boxes_needed(items: u32, per_box: u32) -> u32 separates the logic from the reading and printing, so you can test it in your head with a couple of values before you run anything, and reuse it when the next exercise asks for the same arithmetic. A small read_line() helper hides the stdin().lock().lines().next().unwrap().unwrap() chain, and a function that returns a tuple, such as hours and minutes, is how you hand back two results without inventing a struct too early.

 Rust · in practice
fn read_line() -> String {
    std::io::stdin().lock().lines().next().unwrap().unwrap()
}

fn boxes_needed(items: u32, per_box: u32) -> u32 {
    (items + per_box - 1) / per_box
}

Key points

  • fn name(param: Type) -> Ret { } declares a function; parameter types are never inferred.
  • The last expression of the body, without a semicolon, is the return value; a semicolon makes it a statement.
  • return value; exits early; omitting the arrow means the function returns ().
  • Arguments must match the parameter types exactly; there are no default arguments and no overloading.
  • Return several values as a tuple and destructure them at the call.
  • Take &str for text you only read; callers pass &name for a String.
  • Functions may be declared in any order in the file.

Try it yourself

The program reads a number of items and the capacity of one box. Write a function boxes_needed(items: u32, per_box: u32) -> u32 that returns how many boxes are required, rounding up, and print Boxes: followed by its result.

Your program
use std::io::BufRead;

fn main() {
    let mut lines = std::io::stdin().lock().lines();
    let items: u32 = lines.next().unwrap().unwrap().trim().parse().unwrap();
    let per_box: u32 = lines.next().unwrap().unwrap().trim().parse().unwrap();
    println!("Items: {}", items);
    // define boxes_needed above main and print "Boxes: N" here
}
Input the program receives: 8 ↵ 3
Expected output: Items: 8 Boxes: 3

Practise this

Open the Rust playground

Frequently asked questions

Why does my Rust function return () instead of the value?

Almost always because the last line ends with a semicolon. A semicolon turns an expression into a statement, and a block whose last item is a statement has the value (). Remove the semicolon from the final expression, or write return value; explicitly. The compiler's error message for this case points at the semicolon to remove.

Can a Rust function have default parameter values or be overloaded?

No. Each function has exactly one signature and every argument must be supplied. The usual alternatives are a second function with a more specific name, a parameter of type Option<T> where None means use the default, or a struct that carries the settings. Generic functions can accept several types, but that is a different mechanism from overloading.

Should a Rust function take String or &str?

Take &str whenever the function only reads the text. A &str parameter accepts string literals, slices, and references to a String, so it is the most flexible choice for callers. Take String only when the function needs to keep or modify the text, for example to store it in a struct; the ownership lesson explains why that difference exists.

Progress is stored only in this browser.

How this page was checked. Every program on it was run with rustc 1.94 at build time by the publishing checks, and the output shown is what it printed. Running Rust inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with rustc 1.94 locally.