Code of the Day
BeginnerCore syntax

Functions

Define reusable logic with fn, declare parameter types, return values, and understand the expression-vs-statement distinction that makes Rust functions tick.

RustBeginner10 min read
Recommended first
By the end of this lesson you will be able to:
  • Define a function with fn, typed parameters, and a return type
  • Return a value using an expression at the end of a function body
  • Explain the difference between an expression and a statement in Rust
  • Pass values to functions and use the result

The fundamentals track describes functions as the unit of decomposition — named, reusable pieces of logic. Rust's syntax for them is compact, and there's one nuance that trips up almost everyone coming from another language: expressions vs statements. Understanding this once unlocks a lot of idiomatic Rust.

Defining a function

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

Breaking it down:

  • fn introduces a function definition.
  • add is the name.
  • (a: i32, b: i32) are the parameters — in Rust, every parameter must have an explicit type. The compiler won't infer them.
  • -> i32 is the return type.
  • The body is a + b with no semicolon.

Call it like any other function:

fn main() {
    let result = add(2, 3);
    println!("2 + 3 = {}", result);   // 2 + 3 = 5
}

Expressions vs statements

This is Rust's most important syntactic idea at the beginner level.

A performs an action and returns nothing useful. let x = 5; is a statement — the semicolon ends it.

An evaluates to a value. 2 + 3 is an expression. add(2, 3) is an expression. An if block is an expression. A block { ... } is an expression if its last line has no semicolon.

In the add function above, the last line is a + bno semicolon. That makes it an expression, and its value becomes the function's return value. If you accidentally add a semicolon:

fn add(a: i32, b: i32) -> i32 {
    a + b;   // now a statement — returns ()
}
error[E0308]: mismatched types
 --> src/main.rs:2:24
  |
1 | fn add(a: i32, b: i32) -> i32 {
  |                            ^^^ expected `i32`, found `()`

The compiler expected an i32 but the function returned () (the — nothing). Remove the semicolon.

The missing-semicolon return is idiomatic Rust, not a typo. When you see a function body whose last line has no semicolon, that line is the return value. You can also use return explicitly (return a + b;), but most Rust code uses the implicit form for the final value.

Using return explicitly

return is available and required for early returns:

fn first_positive(values: &[i32]) -> i32 {
    for v in values {
        if *v > 0 {
            return *v;   // early exit
        }
    }
    -1   // default — no semicolon, implicit return
}

Early returns express "we're done, here's the answer" without nesting the rest of the function inside an else.

Functions as documentation

Well-named functions with explicit parameter types are self-documenting. Compare:

// Harder to follow
let r = compute(x, 3600);

// Clear intent
let r = convert_seconds_to_hours(x, 3600);

In Rust, the type signature alone often tells you what a function does and what it won't accept. This is part of the Rust philosophy: make illegal states unrepresentable.

Check your understanding

Knowledge check

  1. 1.
    In Rust, what makes the last line of a function its return value?
  2. 2.
    Rust can infer parameter types in function definitions, so you do not need to annotate them.
  3. 3.
    What does Rust return from a function whose last statement ends with a semicolon, when the declared return type is ()?

Do it yourself

Add this to your project's src/main.rs and run cargo run:

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

fn main() {
    let boiling = celsius_to_fahrenheit(100.0);
    let freezing = celsius_to_fahrenheit(0.0);
    println!("100°C = {}°F", boiling);
    println!("0°C = {}°F", freezing);
}

Then try adding a semicolon after the expression in celsius_to_fahrenheit and see what the compiler says.

Where to go next

Functions are how you structure logic. Next: control flow — making decisions with if/else, repeating work with loop, while, and for, and Rust's range syntax.

Finished reading? Mark it complete to track your progress.

On this page