2.2Function return values

Last updated June 11, 2026

The functions in the last lesson could act, but everything they computed died with them. To participate in their caller's work, functions need to hand values back. In Rust, this is where lesson 1.11 stops being theory.

Return types and return values

A function that produces a value declares the value's type after an arrow:

fn five() -> i32 {
    5
}

fn main() {
    let x = five();
    println!("{}", x + 2);
}
7

-> i32 says "calling this function yields an i32," and the call expression five() evaluates to that value wherever it appears: initializing a variable, sitting inside arithmetic, anywhere an i32 could go.

Now the part Rust does differently, and better. Where's the return keyword? Not needed: a function body is a block, and you already know blocks evaluate to their tail expression. 5 has no semicolon, so it's the tail, so it's the function's value. The 1.11 rule and the function rule are the same rule. A more useful specimen:

fn get_number() -> i32 {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("failed to read input");
    input.trim().parse().expect("that wasn't a whole number")
}

Statements first, then the tail expression delivers the goods. (Two small notes: std::io::stdin() is the long-form spelling that skips the use line, handy in snippets; and parse knows to produce an i32 here because the function's return type says so. Inference reaching that far is very Rust.)

The error you're guaranteed to meet

Because the tail expression does the returning, one reflexive semicolon changes everything:

fn double(x: i32) -> i32 {
    x * 2;
}
error[E0308]: mismatched types
 --> src/main.rs:1:22
  |
1 | fn double(x: i32) -> i32 {
  |    ------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
2 |     x * 2;
  |          - help: remove this semicolon to return this value

Decode it with your 1.11 knowledge: the semicolon discarded x * 2, the body became all-statements, all-statement blocks evaluate to (), and the function promised an i32. The compiler knows this mistake so well that the help line is surgical: remove this semicolon. You will make this typo within days; now it costs you four seconds.

The return keyword

Rust does have return, for exiting before the end:

fn classify(n: i32) -> i32 {
    if n < 0 {
        return -1;
    }
    n * 10
}

(A sneak preview of if, formally lesson 4.7.) For a negative n, the function exits immediately with -1; otherwise it falls through to the tail expression.

Best practice

Use the tail expression for a function's normal result, and return only for early exits. return n * 10; as a final line works, but it reads as an accent; idiomatic Rust ends on the bare expression, and rustfmt-formatted code from the ecosystem will train your eye to expect it.

Functions that return nothing

No arrow means the function returns (), the unit type, like print_warning last lesson and like main itself. Such functions exist for their side effects. You can write -> () explicitly; nobody does.

One consequence worth a sentence: since main returns (), your program signals "success" to the operating system automatically when main ends. Programs that need to signal failure get their tools in lessons 7.8 and 12.3.

And one warning genre to recognize, when return and trailing code mix:

fn answer() -> i32 {
    return 42;
    println!("this line is beyond the end of the world");
}
warning: unreachable statement
 --> src/main.rs:3:5
  |
2 |     return 42;
  |     --------- any code following this expression is unreachable
3 |     println!("this line is beyond the end of the world");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unreachable statement
  |
  = note: `#[warn(unreachable_code)]` (part of `#[warn(unused)]`) on by default

It compiles, the doomed line never runs, and the compiler tells you so. Code after a return is a fossil; delete it or move it above.

The payoff: don't repeat yourself

Remember the chapter 1 finale, with its input-reading recipe pasted twice? Functions retire that duplication:

fn main() {
    println!("Enter a whole number:");
    let x = get_number();
    println!("Enter another whole number:");
    let y = get_number();
    println!("{} + {} is {}", x, y, x + y);
}

fn get_number() -> i32 {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("failed to read input");
    input.trim().parse().expect("that wasn't a whole number")
}
Enter a whole number:
6
Enter another whole number:
4
6 + 4 is 10

The recipe now exists in exactly one place. If it needs improving (and chapter 12 will improve it), there's one place to improve. Programmers call this the DRY principle: Don't Repeat Yourself. Its violation has a name too (WET, "write everything twice"), which tells you how the industry feels about it.

Quiz time

Question #1

What does this program print?

fn seven() -> i32 {
    7
}

fn main() {
    println!("{}", seven() + seven());
}
Show solution
14

Each call evaluates to 7, and the call expressions participate in arithmetic like any other values.

Question #2

Predict the compiler's reaction (error or warning, and roughly what it says):

fn area() -> i32 {
    6 * 7;
}

fn main() {
    println!("{}", area());
}
Show solution

Error E0308, mismatched types: the function promises i32 but the semicolon discards 6 * 7, so the body evaluates to (). The help line says to remove the semicolon. (If you said "expected i32, found ()", full marks.)

Question #3

A function ends with these two lines. What does the compiler say, and does the program run?

    return total;
    println!("done!");
Show solution

It compiles and runs; the println! never executes, and the compiler issues an "unreachable statement" warning pointing at it. The return above it ends the function every time.

Returning values is half of a function's plumbing. The other half flows the opposite way: getting values in. Parameters, next.