Functions
Reviewed & published by Brayan K
Rust is a systems programming language focused on speed, memory safety, and fearless concurrency — and functions are how you package logic into reusable, named building blocks.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll define functions with fn, pass typed parameters, return values, and understand Rust's distinction between statements and expressions.
What You'll Learn in This Lesson
- How to define a function with fn
- How to declare typed parameters
- How to declare a return type with ->
- Why the last expression (no semicolon) is the return value
- The difference between statements and expressions
- When to use the return keyword for early exits
1️⃣ Defining and Calling Functions
You define a function with the fn keyword, a name, a parenthesised list of typed parameters, and an optional -> Type for the return value. Every parameter must have a type annotation. To return a value, make the last line an expression with no semicolon.
// Define a function with fn. Parameters need a type after each name.
// The -> after the parentheses declares the RETURN type.
fn add(a: i32, b: i32) -> i32 {
// The LAST expression with NO semicolon is the return value.
a + b
}
// A function that returns nothing has no -> ... ; its job is a side effect.
fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
greet("Ada"); // -> Hello, Ada!
let sum = add(2, 3); // call add, capture its return value
println!("2 + 3 = {}", sum); // -> 2 + 3 = 5
}Notice add ends with a + b and no semicolon — that expression is the return value. The function greet has no ->, so it returns nothing and exists only to print.
2️⃣ Statements, Expressions, and Early Returns
This is the idea that trips up newcomers: in Rust a block { } is itself an expression that evaluates to its last value. That means if blocks and even nested blocks can produce values. The return keyword still exists for leaving a function early.
fn main() {
// In Rust, a block { } is an EXPRESSION — it evaluates to a value.
// The last line without a semicolon becomes the block's value.
let y = {
let inner = 3;
inner * inner // no semicolon -> this is the block's value (9)
};
println!("y = {}", y); // -> y = 9
// "return" can be used for an early exit, but the last expression is idiomatic.
let label = describe(7);
println!("7 is {}", label); // -> 7 is odd
}
fn describe(n: i32) -> &'static str {
if n % 2 == 0 {
return "even"; // early return for the even case
}
// Otherwise fall through and build the final value.
if n < 0 { "negative odd" } else { "odd" }
}
// Note: this prints with the number, see main where we format it.The block assigned to y evaluated to 9 because inner * inner had no semicolon. In describe, the early return handles the even case, and the final if expression provides the rest.
Your turn. Fill in the blanks marked ___, then run it.
// 🎯 YOUR TURN — finish the functions, then run it.
// 1) Complete this function so it returns the SQUARE of n.
fn square(n: i32) -> i32 {
___ // 👉 an expression with NO semicolon, e.g. n * n
}
// 2) Complete the return type so this returns an i32.
fn double(n: i32) -> ___ { // 👉 the return type
n * 2
}
fn main() {
println!("square(4) = {}", square(4)); // expected: 16
println!("double(4) = {}", double(4)); // expected: 8
}
// ✅ Expected output:
// square(4) = 16
// double(4) = 8Mini-Challenge: A Rectangle Toolkit
Write two functions yourself and call them from main. Run it with cargo run and check the output.
// 🎯 MINI-CHALLENGE: a tiny rectangle toolkit.
//
// 1. Write fn area(width: u32, height: u32) -> u32 that returns width*height.
// 2. Write fn perimeter(width: u32, height: u32) -> u32 returning 2*(w+h).
// 3. In main, call both with width 4 and height 3.
// 4. Print "Area: 12" and "Perimeter: 14".
//
// Remember: the last expression with NO semicolon is the return value.
//
// ✅ Example output:
// Area: 12
// Perimeter: 14
fn main() {
// your code here
}Common Errors (and the fix)
- "mismatched types: expected `i32`, found `()`" — You put a semicolon after the final expression, turning the return value into nothing. Remove the trailing semicolon.
- "expected type, found ..." — A parameter is missing its type. Every parameter needs one: fn f(x: i32).
- Forgetting the return type — If a function produces a value, declare it: fn f() -> i32. Without -> Rust expects no return value.
- Calling before all args are given — Rust has no default arguments. Pass every parameter the signature lists.
Pro Tips
- 💡 Prefer the final expression: ending a function with an unsemicoloned expression is the idiomatic way to return.
- 💡 Functions can be declared in any order: Rust doesn't care if main calls a function defined below it.
- 💡 Keep functions small: a function that does one clear thing is easier to test and reuse.
📋 Quick Reference — Functions
| Goal | Rust Syntax |
|---|---|
| Define a function | fn name(x: i32) { } |
| Return a value | fn f() -> i32 { 5 } |
| Call a function | let r = f(); |
| Early return | return value; |
| Returns nothing | fn f() { } |
🎉 Lesson Complete!
- ✅ Define functions with fn name(params) -> Type
- ✅ Every parameter needs an explicit type
- ✅ The last expression with no semicolon is the return value
- ✅ Blocks { } are expressions; return exits early
Practice quiz
Which keyword defines a function in Rust?
- func
- def
- fn
- function
Answer: fn. Functions are defined with the fn keyword, e.g. fn add(a: i32, b: i32) -> i32.
What declares a function's return type?
- An arrow -> after the parentheses
- A colon before the body
- The returns keyword
- A => symbol
Answer: An arrow -> after the parentheses. The -> after the parameter list declares the return type, e.g. fn f() -> i32.
What becomes a function's return value?
- The first line
- Any line with return only
- The function name
- The last expression with no semicolon
Answer: The last expression with no semicolon. The last expression in the body, with no trailing semicolon, is the return value.
What happens if you add a semicolon after the final expression a + b?
- Nothing changes
- It becomes a statement evaluating to (), often causing a type mismatch
- It returns twice
- It panics
Answer: It becomes a statement evaluating to (), often causing a type mismatch. A trailing semicolon turns the expression into a statement returning (), so a fn meant to return i32 won't compile.
Must every function parameter have an explicit type?
- Yes, every parameter must be annotated
- No, types are inferred
- Only the first one
- Only for return values
Answer: Yes, every parameter must be annotated. Unlike local variables, function parameters always require explicit type annotations.
What does a function with no -> in its signature return?
- i32
- null
- The unit type ()
- An error
Answer: The unit type (). Without ->, a function returns the unit type (), meaning no meaningful value.
In Rust, a block { } is...
- only a grouping of statements
- an expression that evaluates to its last value
- always empty
- the same as a loop
Answer: an expression that evaluates to its last value. A block is an expression; its value is the last expression inside it (with no semicolon).
What is the value of let y = { let inner = 3; inner * inner };?
- 3
- 6
- ()
- 9
Answer: 9. The block evaluates to inner * inner = 9 because that line has no semicolon.
When should you use the return keyword?
- Always, at the end
- For an early exit from the middle of a function
- Never
- Only in main
Answer: For an early exit from the middle of a function. Use return to exit early; the idiomatic happy-path result is a final expression with no semicolon.
Does Rust support default argument values for functions?
- Yes
- Only for the last parameter
- No — you must pass every parameter the signature lists
- Only with a macro
Answer: No — you must pass every parameter the signature lists. Rust has no default arguments; every parameter in the signature must be provided at the call site.
Continue this course
- Previous: String Formatting (format!, println!)
- Next: Control Flow — Branch with if/else and loop with loop, while, and for
- Quick reference: Rust cheat sheet › Basics
Frequently asked questions
Why does removing a semicolon change what a function returns?
In Rust, statements (ending in a semicolon) produce no value, while expressions produce a value. The last line of a function body, if it is an expression with no trailing semicolon, becomes the function's return value. Adding a semicolon turns it into a statement that evaluates to the empty unit type (), which usually causes a type-mismatch error if the function is supposed to return something. So 'a + b' returns the sum, but 'a + b;' returns nothing.
When should I use the return keyword instead of a final expression?
Use a final expression (no semicolon) for the normal, happy-path result — it is the idiomatic Rust style. Use the return keyword when you want to exit early from the middle of a function, for example to bail out as soon as you detect a special case. Both produce the same kind of value; return just lets you leave before reaching the end of the body.
Do I always have to annotate parameter and return types?
Yes for function signatures. Unlike local variables, where Rust can often infer the type, every function parameter must have an explicit type and every function that returns a value must declare its return type with ->. This is intentional: it makes function signatures a reliable contract, keeps error messages clear, and means a function's interface never depends on guessing.
What does a function return if it has no -> in its signature?
It returns the unit type, written (), which is Rust's way of saying 'no meaningful value'. Functions like one that only prints to the screen return (). You do not normally write this out; omitting the -> simply means the function exists for its side effects rather than for a value you capture.