Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Rust (2024 edition)  Module 2 – Values, types, functions and control flow

Functions, blocks and expressions

Write Rust functions with typed parameters and results, use blocks and if as values, return early, and see why one stray semicolon breaks a function.

  • Beginner
  • 20 minutes
  • Examples run with Rust 1.99.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: A function states the type of every parameter and of its result · A block is an expression, and its last expression is its value · leaves a function early. With a pass: 2 in-depth parts and 3 interview answers, about 12 minutes.

What you will learn

  • Define functions with typed parameters and a result type, and call them from anywhere in their file
  • Distinguish statements from expressions, and use a block or an if as a value
  • Fix error E0308 caused by a semicolon after a function's last expression
  • Use return to leave a function early, and predict what panic!, todo! and process::exit do instead of returning
  • Write a function in expression style, without mutable variables or a final return

Before you start

What you’ll learn · Before you start

What you will learn

  • Define functions with typed parameters and a result type, and call them from anywhere in their file
  • Distinguish statements from expressions, and use a block or an if as a value
  • Fix error E0308 caused by a semicolon after a function's last expression
  • Use return to leave a function early, and predict what panic!, todo! and process::exit do instead of returning
  • Write a function in expression style, without mutable variables or a final return

Before you start

On this page

Remove one character from a three-line Rust function and it compiles; put it back and rustc reports E0308, mismatched types: expected u32, found (). The character is a semicolon, and the error makes sense once you know that almost everything in Rust is an expression that has a value.

This lesson writes functions with typed parameters and results, uses blocks and if as values, and returns early when an input makes no sense, with a train fare worked out from distance bands as the running example. The fares are made up for the lesson, and every result is a run on Rust 1.99.0.

A function states the type of every parameter and of its result

Rust functions begin with fn, then the name, the parameters each with its type, and, after ->, the type of the value the function returns. This program defines three functions and calls each one:

functions.rs: three functions and their calls Rust · functions.rs
// Three functions: one with a result, one with two results, and one with none.
fn platform_fee(adults: u32, children: u32) -> u32 {
    adults * 1_000 + children * 500
}

fn main() {
    let fee = platform_fee(2, 1);
    println!("platform fee: {fee} paise");
    let (each, left_over) = split(fee, 3);
    println!("split three ways: {each} paise each, {left_over} left over");
    announce(fee);
}

// Defined after main, which calls it: the order of functions in a file does not matter.
fn split(total: u32, people: u32) -> (u32, u32) {
    (total / people, total % people)
}

fn announce(fee: u32) {
    println!("announced: the fee is {fee} paise");
}

Output

platform fee: 2500 paise
split three ways: 833 paise each, 1 left over
announced: the fee is 2500 paise

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O functions.rs && ./functions

platform_fee takes two u32 values and returns one. split returns two values at once, as a tuple, so the caller takes them apart with a pattern. announce has no -> at all, which means it returns (), the unit value. And main calls split and announce before their definitions in the file: functions are items, and the compiler finds them wherever they are in their module. The types of parameters are never inferred, and a missing one is a syntax error:

untyped_param.rs: a parameter without its type Rust · untyped_param.rs
// A parameter without its type.
fn platform_fee(adults) -> u32 {
    adults * 1_000
}

fn main() {
    println!("{}", platform_fee(2));
}

Output (exit status 1)

Printed as an error (standard error)

error: expected one of `:`, `@`, or `|`, found `)`
 --> untyped_param.rs:2:23
  |
2 | fn platform_fee(adults) -> u32 {
  |                       ^ expected one of `:`, `@`, or `|`
  |
help: if this is a parameter name, give it a type
  |
2 | fn platform_fee(adults: TypeName) -> u32 {
  |                       ++++++++++
help: if this is a type, explicitly ignore the parameter name
  |
2 | fn platform_fee(_: adults) -> u32 {
  |                 ++

error: aborting due to 1 previous error

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O untyped_param.rs && ./untyped_param

The help suggests the fix, a type after the name. Requiring types on every parameter is deliberate: rustc can then check each function on its own, and a reader learns what it takes from its first line. A name also means one function: Rust has no overloading by parameter types and no default values for parameters.

A block is an expression, and its last expression is its value

Rust distinguishes statements, which do something, from expression: A piece of code that produces a value, such as 2 + 3, a function call, an if or a block. A block's last expression, written without a semicolon, is the value of the whole block., which produce a value. let base = 2_000; is a statement; base + extra is an expression; and blocks are expressions too, made of statements and an optional last expression without a semicolon, the block’s value:

blocks.rs: a block and an if used as values Rust · blocks.rs
// A block and an if, each used as a value.
fn main() {
    let km: u32 = 120;
    let fare = {
        let base = 2_000;
        let extra = (km - 50) * 60;
        base + extra
    };
    let kind = if km > 50 { "regional" } else { "local" };
    println!("{km} km: {kind}, fare {fare} paise");
}

Output

120 km: regional, fare 6200 paise

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O blocks.rs && ./blocks

fare got its value, 6200, from the block’s last line, and kind got "regional" from an if, which is an expression as well: each branch is a block, and the branch that runs gives the if its value. So both branches must have the same type. Step through how the block becomes a value, and what one semicolon would change:

Interactive · stage by stage How the block in blocks.rs becomes a value Step through the block line by line. Watch which lines are statements and which one gives the block its value.
How the block in blocks.rs becomes a value, step 5 of 5. Showing let base = 2_000; (base is 2000), let extra = …; (extra is 4200), base + extra (with ;, no tail at all), let fare = { … }; (fare would be ()). In focus: with-semicolon, base + extra.with a ;, the value ()let base = 2_000;base is 2000let extra = …;extra is 4200base + extrawith ;, no tail at alllet fare = { … };fare would be ()

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. A statement. let base = 2_000; is a statement: it binds base to 2000 and has no value of its own. The block goes on to the next line.
  2. Another statement. let extra = (km - 50) * 60; works out (120 − 50) × 60 = 4200 and binds it. A let is always a statement.
  3. The tail expression. base + extra has no semicolon after it, so it is the block's tail: its value, 6200, becomes the value of the whole block.
  4. The block is a value. The block stands where a value is expected, after let fare =, so fare becomes 6200. The names base and extra end with the block; only its value survives.
  5. One semicolon more. Write base + extra; and it becomes a statement too. The block then ends without a tail, and its value is (), the unit value; in a function that promised a u32, that is the E0308 of semicolon.rs.

A function body is a block, so the same rule decides what a function returns. With a semicolon after its last expression, the body ends in a statement, and its value is ():

semicolon.rs: a stray semicolon Rust · semicolon.rs
// A reservation charge added to a fare, with one character too many.
fn with_reservation(fare: u32) -> u32 {
    fare + 4_000;
}

fn main() {
    println!("{}", with_reservation(6_200));
}

Output (exit status 1)

Printed as an error (standard error)

error[E0308]: mismatched types
 --> semicolon.rs:2:35
  |
2 | fn with_reservation(fare: u32) -> u32 {
  |    ----------------               ^^^ expected `u32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
3 |     fare + 4_000;
  |                 - help: remove this semicolon to return this value

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0308`.

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O semicolon.rs && ./semicolon

rustc says both halves of the story: the function “implicitly returns () as its body has no tail or return expression”, and the help points at the semicolon to remove.

Ending a function's value with a semicolon

fare + 4_000; throws the sum away and leaves the function with (). Write the last expression without a semicolon, fare + 4_000, and it becomes the function’s result.

Checkpoint

Check: Blocks and if as values

  1. Question 1 of 2 What is x after let x = { let a = 3; let b = a + 1; a * b };?

    Type a number.

    A hint

    The block's value is its last expression, the one without a semicolon.

    Show the answer: Question 1 of 2

    Answer: 12

    The two let lines are statements that bind a = 3 and b = 4. The last line, a * b, has no semicolon, so its value, 12, is the value of the block and of x.

  2. Question 2 of 2 This function fails with E0308, expected u32, found (). What is the smallest fix?

    Read the code, then choose one answer.

    fn with_reservation(fare: u32) -> u32 {
        fare + 4_000;
    }
    Show the answer: Question 2 of 2

    Answer: Remove the semicolon after fare + 4_000

    A function returns its body's last expression. The semicolon turned the sum into a statement whose value is thrown away, so the body ended with (). Removing it makes the sum the result.

return leaves a function early

Most functions end by producing their last expression, but some inputs should stop a function before it gets there. This fare function refuses a distance of 0 or more than 5,000 km with return, and works out the rest with an if chain whose value becomes the fare:

early.rs: a fare by distance band, with an early return Rust · early.rs
// A fare by distance band, with an early return for a distance that makes no sense.
fn fare_paise(km: u32) -> Option<u32> {
    if km == 0 || km > 5_000 {
        return None; // nothing sensible to charge
    }
    let fare = if km <= 50 {
        2_000
    } else if km <= 200 {
        2_000 + (km - 50) * 60
    } else {
        11_000 + (km - 200) * 45
    };
    Some(fare)
}

fn main() {
    for km in [0, 30, 120, 260, 6_000] {
        println!("{km:>5} km: {:?}", fare_paise(km));
    }
}

Output

    0 km: None
   30 km: Some(2000)
  120 km: Some(6200)
  260 km: Some(13700)
 6000 km: None

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O early.rs && ./early

The result type, Option<u32>, says the function gives either Some(fare) or None, no fare at all; the module on enums covers Option in full. return None; leaves the function at once with that value. For every other distance, the if chain is an expression whose value is bound to fare, and Some(fare), the last expression, is the result. That is the usual shape of Rust functions: return for the early exits, and a last expression, without return, for the normal result.

Some expressions never produce a value at all. panic! stops the thread, todo!() panics with “not yet implemented”, and std::process::exit ends the whole process. Rust gives them the never type, !.

Key idea

A function’s result is its body’s last expression, written without a semicolon; return is for leaving early, and an if or a block can produce the value itself.

Checkpoint

Check: Returning early · Blocks and if as values

  1. Question 1 of 2 What does fare_paise(6_000) in early.rs return, and which line produces it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: None, from return None; at the start of the function

    if km == 0 || km > 5_000 { return None; } leaves the function at once with None, as the recorded run shows for 0 and 6000 km.

  2. Question 2 of 2 Why does let kind = if km > 50 { "regional" } else { 0 }; fail to compile?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: An if used as a value must give the same type in every branch, and a &str is not an integer

    An if expression has one type. "regional" is a &str and 0 an integer, so no single type fits both branches, and the compiler refuses the line with a type mismatch.

Expressions that never return, and what they skip Pro: opens with a Pro, Premium or Ultimate pass

Some expressions never produce a value: they stop the thread or end the whole process. This part runs three of them, shows why they fit wherever a value is expected, and measures what ending a program with process::exit costs.

In this part:

  • Three ways to never return, run
  • Why if loaded { 2_000 } else { panic!(…) } has a type
  • What process::exit costs a program with a buffered file, measured
  • Choosing between a panic, an exit and a returned error

4 runnable examples (Python, Rust) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Expression style, Rust's answer to overloading, and the fare exercise Pro: opens with a Pro, Premium or Ultimate pass

Idiomatic Rust lets values come out of blocks and if expressions, and keeps return for early exits. This part runs one function through Clippy, shows what Rust APIs offer instead of overloading and default arguments, and ends with an exercise in expression style.

In this part:

  • The same fare in two styles, and what Clippy and rustc say
  • No overloading or default arguments: what Rust APIs do instead
  • Functions inside functions, and functions the compiler runs
  • Exercise: a fare by band and class, with no mutable variables

5 runnable examples (Python, Rust) · about 5 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Practise: functions, blocks and early returns

These questions are about function signatures, blocks and if as values, the stray semicolon and early returns. Every choice comes with the reason it is right or wrong.

Practice that adapts to you

Practise this lesson

11 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 11 Which line declares a function that takes two u32 values and returns a u32?

    Choose one answer.

    Show the answer: Question 1 of 11

    Answer: fn fee(adults: u32, children: u32) -> u32

    A Rust signature is fn, the name, each parameter as name: Type, and -> with the result type.

  2. Question 2 of 11 What does a function written fn announce(fee: u32) { … } return?

    Choose one answer.

    Show the answer: Question 2 of 11

    Answer: (), the unit value

    A function with no -> returns the unit value (), the empty tuple of the last lesson.

  3. Question 3 of 11 In functions.rs, main calls split, which is defined below main. What happens when it is built?

    Choose one answer.

    Show the answer: Question 3 of 11

    Answer: It builds, because functions are items found anywhere in their module

    Functions are items, and the compiler finds every item of a module wherever it is written, so a call may come before the definition.

  4. Question 4 of 11 A function must give back a quotient and a remainder. What is the usual way to return both?

    Choose one answer.

    Show the answer: Question 4 of 11

    Answer: Return a tuple, such as -> (u32, u32), and let the caller destructure it

    A tuple groups the results, and let (each, left_over) = split(fee, 3); names them at the call site.

  5. Question 5 of 11 Which of these is a statement rather than an expression?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: let base = 2_000;

    Statements do something: a let binds, and an expression followed by a semicolon is evaluated and its value thrown away. Blocks, ifs, calls and operations are expressions.

  6. Question 6 of 11 What value does total get?

    Read the code, then choose one answer.

    let total = {
        let a = 2;
        let b = 3;
        a * b;
    };
    Show the answer: Question 6 of 11

    Answer: (), because the block ends with a statement

    A block's value is its last expression only if it has one without a semicolon. Here it ends with a statement, so total is the unit value.

  7. Question 7 of 11 In blocks.rs, km becomes 30. What is kind?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: "local"

    An if used as a value gives the value of the branch that runs. Both branches have the same type, &str, which is why the line compiles.

  8. Question 8 of 11 What does fare_paise(0) in early.rs return?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: None

    return None; leaves the function before any band is worked out, for 0 km and for more than 5,000 km.

  9. Question 9 of 11 What does fare_paise(150) in early.rs return, in paise?

    Type a number.

    A hint

    150 km falls in the second band, 2,000 plus 60 for every kilometre over 50.

    Show the answer: Question 9 of 11

    Answer: 8000 paise

    150 is above 50 and at most 200, so the second branch of the if chain runs: 2,000 + (150 − 50) × 60 = 8,000, returned as Some(8000).

  10. Question 10 of 11 Which of these never produces a value at all?

    Choose one answer.

    Show the answer: Question 10 of 11

    Answer: std::process::exit(2)

    panic!, todo!() and process::exit never finish, so they never produce a value; Rust gives them the never type, written !.

  11. Question 11 of 11 Where does return belong in idiomatic Rust?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: In early exits, such as refusing a bad input; the normal result is the last expression, without return

    A function's normal result is its body's last expression. return is for leaving early, when the rest of the function should not run.

Interview questions

Warm-up (fresher): what is the difference between a statement and an expression in Rust?

An expression produces a value: 2 + 3, a function call, an if, a block. A statement does something and has no value of its own: a let statement binds a name, and an expression followed by a semicolon is an expression statement, whose value is thrown away. Rust is expression-based, so a block’s last expression, written without a semicolon, is the block’s value, and a function returns its body’s last expression without return. That is why x + 1; at the end of a function that promises a u32 is error E0308: the semicolon turns the result into a statement, and the body’s value becomes ().

  1. Mid to senior

    Rust has no default arguments and no overloading. A fare function is usually called with a distance only, sometimes with a class, and rarely with a class, a concession and a travel date. How would you shape its API, and why?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A command-line tool writes a CSV report through a BufWriter. Deep inside, a parse function calls std::process::exit(1) on a malformed row. Users say the report is sometimes empty, even of rows before the bad one. Why, and how would you restructure the error path?

    Model answer, Pro. First step and follow-ups

  3. Fresher to mid

    A teammate writes fn fare(km: u32) -> u32 that calls panic!("bad distance") when km is 0, because "it should never happen". The function is called from a booking API. What would you change, and when is a panic the right call?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Rust functions write out every parameter’s type and their result’s after ->; without -> they return ().
  • Blocks and if are expressions: a block’s value is its last expression without a semicolon, and both branches of an if must have the same type.
  • A semicolon after a function’s last expression makes its value (), which is error E0308 when the function promises another type.
  • return leaves a function early; the normal result is the last expression, and panic!, todo!() and process::exit never return at all.

Terms in this lesson

expression
A piece of code that produces a value, such as 2 + 3, a function call, an if or a block. A block's last expression, written without a semicolon, is the value of the whole block.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.