Rust functions
last modified October 25, 2026
In this article we show how to define and use functions in Rust.
A function is a named block of code that can be called from other parts of the program. Functions split a program into small, reusable pieces and give names to its operations.
A function is defined with the fn keyword, followed by its name,
a list of parameters in parentheses, an optional return type after
->, and a body in curly braces. By convention, function names
use snake_case. Every Rust program starts in the main function.
The types of the parameters and of the return value must always be written
out; they are not inferred.
The examples were tested with Rust 1.97 and the 2024 edition. They are run as
Cargo projects with cargo run -q.
A simple function
The first example defines a function without parameters and without a return value.
fn main() {
print_msg();
print_msg();
}
fn print_msg() {
println!("Hello there!");
}
In the program, we define two functions. The main function calls
the print_msg function twice.
fn print_msg() {
println!("Hello there!");
}
The print_msg function prints a message to the console. A
function can be defined before or after the place where it is called.
$ cargo run -q Hello there! Hello there!
Function parameters
Parameters are listed in the parentheses after the function name; each one has a name and a type.
fn main() {
let name = String::from("John Doe");
let age = 34;
print_info(&name, age);
print_info("Roger Roe", 47);
println!("{name} is still available");
}
fn print_info(name: &str, age: u32) {
println!("{name} is {age} years old");
}
The print_info function takes a name and an age.
fn print_info(name: &str, age: u32) {
The name is a string slice, &str. The function only reads
the text, so it borrows it instead of taking ownership. Such a parameter
accepts string literals as well as borrowed String values.
print_info(&name, age);
print_info("Roger Roe", 47);
We pass a reference to a String and a string literal. Since
name was only borrowed, it can still be used after the call. Had
the parameter been of type String, the value would be moved into
the function. See Rust ownership for
details.
$ cargo run -q John Doe is 34 years old Roger Roe is 47 years old John Doe is still available
Return values
The return type is written after an arrow. The value of the last expression in the function body is the return value of the function.
fn main() {
let res = square(7);
println!("{res}");
let msg = build_msg("John Doe", 34);
println!("{msg}");
}
fn square(x: i32) -> i32 {
x * x
}
fn build_msg(name: &str, age: u32) -> String {
format!("{name} is {age} years old")
}
The program defines two functions that return a value.
fn square(x: i32) -> i32 {
x * x
}
The square function returns an i32. The expression
x * x has no semicolon; it is the tail expression of the body,
and its value is returned. Adding a semicolon would turn it into a statement,
and the compiler would report a type mismatch, because the body would then
evaluate to the unit type ().
fn build_msg(name: &str, age: u32) -> String {
format!("{name} is {age} years old")
}
The build_msg function returns a new String created
by the format! macro.
$ cargo run -q 49 John Doe is 34 years old
Early return
The return keyword ends the function immediately with the given
value. It is used to leave a function before its end.
fn main() {
println!("{}", classify(-4));
println!("{}", classify(0));
println!("{}", classify(13));
}
fn classify(n: i32) -> &'static str {
if n < 0 {
return "negative";
}
if n == 0 {
return "zero";
}
"positive"
}
The classify function returns a string slice describing the
number.
if n < 0 {
return "negative";
}
For negative numbers, the function returns right away and the rest of the body is skipped.
"positive"
The last line is the tail expression; it is used when none of the early
returns was taken. The &'static str return type means the
returned text lives for the whole run of the program, which is true for string
literals.
$ cargo run -q negative zero positive
Returning multiple values
A function returns a single value, but that value can be a tuple. This is the usual way to return several values.
fn main() {
let vals = [4, -2, 9, 0, 7];
let (min, max) = min_max(&vals);
println!("min: {min}, max: {max}");
}
fn min_max(vals: &[i32]) -> (i32, i32) {
let mut min = vals[0];
let mut max = vals[0];
for &v in vals {
if v < min {
min = v;
}
if v > max {
max = v;
}
}
(min, max)
}
The min_max function finds the smallest and the largest value of
a slice.
fn min_max(vals: &[i32]) -> (i32, i32) {
The function takes a slice of integers and returns a pair of integers.
let (min, max) = min_max(&vals);
The returned tuple is destructured into two variables.
$ cargo run -q min: -2, max: 9
Mutable reference parameters
To let a function modify a value of the caller, we pass a mutable reference.
fn main() {
let mut words = vec![String::from("sky")];
add_word(&mut words, "cup");
add_word(&mut words, "forest");
println!("{words:?}");
}
fn add_word(words: &mut Vec<String>, word: &str) {
words.push(word.to_string());
}
The add_word function appends a word to a vector.
fn add_word(words: &mut Vec<String>, word: &str) {
words.push(word.to_string());
}
The &mut Vec<String> parameter is a mutable borrow of
the vector. The function can change it, while the caller keeps the
ownership.
add_word(&mut words, "cup");
The caller must write &mut explicitly, so it is visible at
the call site that the function may change the vector. The variable itself
must be declared with mut.
$ cargo run -q ["sky", "cup", "forest"]
Recursion
A recursive function calls itself. The computation stops at a base case that does not call the function again.
fn main() {
for n in [0, 1, 5, 10, 20] {
println!("{n}! = {}", factorial(n));
}
}
fn factorial(n: u64) -> u64 {
if n == 0 { 1 } else { n * factorial(n - 1) }
}
The program computes the factorial of several numbers.
fn factorial(n: u64) -> u64 {
if n == 0 { 1 } else { n * factorial(n - 1) }
}
The factorial of 0 is 1; this is the base case. For other values, the
function multiplies n by the factorial of n - 1. The
whole body is a single if expression whose value is returned.
The factorial of 20 is the largest one that fits into a u64.
$ cargo run -q 0! = 1 1! = 1 5! = 120 10! = 3628800 20! = 2432902008176640000
Functions as values
Functions can be stored in variables and passed to other functions. The type
of a function pointer is written as fn(i32) -> i32.
fn main() {
println!("{}", apply(double, 5));
println!("{}", apply(negate, 5));
let ops = [double, negate, square];
for f in ops {
print!("{} ", f(10));
}
println!();
}
fn double(x: i32) -> i32 {
x * 2
}
fn negate(x: i32) -> i32 {
-x
}
fn square(x: i32) -> i32 {
x * x
}
fn apply(f: fn(i32) -> i32, val: i32) -> i32 {
f(val)
}
We define three simple functions and a function that applies another function to a value.
fn apply(f: fn(i32) -> i32, val: i32) -> i32 {
f(val)
}
The apply function takes a function pointer and calls it with the
given value.
let ops = [double, negate, square];
for f in ops {
print!("{} ", f(10));
}
The three functions have the same signature, so they can be stored in an array. We loop over the array and call each function.
$ cargo run -q 10 -5 20 -10 100
Closures
A closure is an anonymous function that can capture variables from its environment. Its parameters are written between vertical bars, and their types are usually inferred.
fn main() {
let num = 3;
let add_num = |x| x + num;
println!("{}", add_num(5));
let mut counter = 0;
let mut inc = || counter += 1;
inc();
inc();
println!("counter: {counter}");
let words = ["sky", "forest", "cup", "water"];
let min_len = 4;
let long: Vec<_> = words.iter().filter(|w| w.len() >= min_len).collect();
println!("{long:?}");
println!("{}", apply_twice(|x| x * 3, 2));
}
fn apply_twice(f: impl Fn(i32) -> i32, val: i32) -> i32 {
f(f(val))
}
The program creates several closures.
let num = 3; let add_num = |x| x + num;
The closure captures the num variable from the enclosing
function. A regular fn cannot do that.
let mut counter = 0; let mut inc = || counter += 1;
This closure modifies a captured variable, so it borrows
counter mutably; the closure variable must be declared with
mut. After the last call of inc, the borrow ends and
we can print the counter.
let long: Vec<_> = words.iter().filter(|w| w.len() >= min_len).collect();
Closures are often passed to iterator methods. Here the closure uses the
captured min_len value to filter the words.
fn apply_twice(f: impl Fn(i32) -> i32, val: i32) -> i32 {
f(f(val))
}
A function that accepts closures uses one of the Fn,
FnMut, or FnOnce traits. The impl Fn(i32)
-> i32 parameter accepts any closure or function with this
signature.
$ cargo run -q 8 counter: 2 ["forest", "water"] 18
Source
The Rust Programming Language: Functions
The Rust Programming Language: Closures
In this article we have worked with functions in Rust.
Author
List all Rust tutorials.