Functions

Write functions with parameters and return values, and learn the one quirk — expressions versus statements — that trips up almost everyone coming from another language.

10 min read•4 learning objectives

What You'll Learn

  • Define functions with parameters and return types
  • Understand the difference between statements and expressions
  • Return values without writing the word "return"
  • Follow Rust naming conventions that the compiler will nudge you toward anyway

You've already written one function — main, the entry point every Rust program needs. Let's go further: write your own, pass data into them, and get values back out.

Defining a function

Use the fn keyword, a name, parentheses for parameters, and curly braces for the body. Rust doesn't care where in the file a function is defined relative to where it's called — unlike some languages, there's no "must be declared before use" rule.

Rust
fn main() {
    greet();
}

fn greet() {
    println!("Hey there!");
}

Notice greet is defined after it's used in main. That's perfectly fine — Rust reads the whole file before deciding whether anything is wrong.

A note on naming

Rust convention is snake_case for function and variable names — calculate_total, not calculateTotal or CalculateTotal. This isn't just a style guide suggestion: the compiler will actually print a warning if you stray from it. Rust has opinions, and honestly, that consistency makes other people's code easier to read on day one.

Parameters

Parameters need explicit types — always. This is one place Rust won't infer anything for you, and that's by design: a function's signature is a contract, and contracts should be unambiguous to anyone reading just the one line.

Rust
fn main() {
    greet_user("Sora");
    print_total(3, 19.99);
}

fn greet_user(name: &str) {
    println!("Welcome, {name}!");
}

fn print_total(quantity: u32, price: f64) {
    let total = quantity as f64 * price;
    println!("{quantity} item(s) at ${price} each = ${total}");
}

Return values

Here's the part that surprises people coming from other languages: in Rust, the last expression in a function is automatically what it returns — no return keyword required. You just... don't put a semicolon on it.

Rust
fn square(x: i32) -> i32 {
    x * x   // no semicolon — this is the return value
}

fn main() {
    let result = square(7);
    println!("{result}"); // 49
}

The -> i32 after the parentheses declares the return type. You can still use return explicitly — it's useful for returning early from inside an if or a loop — but for the final value of a function, leaving off the semicolon is the idiomatic Rust way.

The real key: statements vs. expressions

That semicolon trick isn't a quirk — it's a direct consequence of a deeper idea that runs through the entire language. Rust draws a hard line between two kinds of things:

  • Statements perform an action and return nothing. let x = 5; is a statement — it binds a value to a name, full stop.
  • Expressions evaluate to a value. 5 + 6 is an expression. So is a function call. So, importantly, is a block of code wrapped in { }.

That last one is the part that unlocks everything. A { } block evaluates to whatever its final, semicolon-free expression evaluates to — and you can use that block anywhere a value is expected:

Rust
fn main() {
    let y = {
        let x = 3;
        x + 1   // no semicolon — the block evaluates to 4
    };

    println!("y is {y}"); // y is 4
}

Adding a semicolon turns an expression into a statement — and statements evaluate to nothing. This is exactly why a function body that ends with x * x; (semicolon included) would fail to compile if the function promised to return an i32: you've turned your return value into "nothing," and Rust will tell you so, clearly, at compile time.

Try it yourself: add a semicolon to the end of x * x in the square example above and try to run it. Read the compiler error carefully — it will tell you almost exactly what's wrong and how to fix it. Learning to enjoy reading Rust's error messages is a real career skill.

Putting it together

Once this clicks, you start writing more naturally — fewer temporary variables, fewer explicit returns, more "this block is just an expression that produces a value":

Rust
fn describe_temperature(celsius: i32) -> String {
    let feeling = if celsius < 0 {
        "freezing"
    } else if celsius < 15 {
        "chilly"
    } else if celsius < 25 {
        "pleasant"
    } else {
        "hot"
    };

    format!("{celsius}°C feels {feeling}")
}

fn main() {
    println!("{}", describe_temperature(22));
    println!("{}", describe_temperature(-5));
}

See how the entire if/else if chain is itself an expression, assigned directly to feeling? No temporary mutable variable, no repeated assignments in each branch. We'll dig much deeper into if and friends in the next lesson.

Quick exercise

Write a function fn is_even(n: i32) -> bool that returns whether a number is even, without using the word return anywhere. Then write a second function that takes a slice of numbers and returns how many of them are even, calling your first function from inside it.

You can now write and call your own functions, and — more importantly — you understand why Rust code looks the way it does. That single idea, "blocks are expressions," will keep paying off as we go.