Function and Method Calls
This document explains how function and method call expressions work in Rust, covering syntax, evaluation, and return types.
A function call or method call is an expression that asks Rust to execute a piece of named code with specific inputs and then hand you back whatever that code produces. Because Rust is expression-oriented, every call produces a value — even if that value is the zero-sized () (the "unit" type) when the function returns nothing. This design means you can drop a call anywhere an expression is expected: as the right side of a let binding, as an argument to another call, or inside a block whose final value becomes its return.
Calling Functions
The most direct way to invoke a callable is with the function call syntax: the function's path followed by parentheses containing any arguments.
fn greet(name: &str) -> String {
format!("Hello, {name}!")
}
fn main() {
let message = greet("Rust"); // function call expression
println!("{message}");
}
The type of the expression greet("Rust") is String because that is what the function signature declares after the ->. If the function had no ->, the call would evaluate to ().
Arguments you pass must match the parameters in number and type. Rust requires an exact match — there is no implicit numeric coercion or default values. Each argument expression is evaluated before the function body begins. The compiler is free to choose the evaluation order of arguments, so relying on side effects that depend on a specific ordering is unsafe.
When the function name is a simple path like greet, the compiler can directly resolve it to a function item. Rust also lets you call function pointers and closures with the same syntax because they all implement one of the call traits (Fn, FnMut, or FnOnce). The expression behaves identically from the caller's perspective.
fn add(a: i32, b: i32) -> i32 {
a + b
}
// Function pointer call — same syntax.
let op: fn(i32, i32) -> i32 = add;
let result = op(10, 20); // 30
Function items vs function pointers:
A plain function name like add in the code above is a function item type, not directly a pointer. Rust coerces function items to function pointers silently when needed, so the call op(10, 20) works because op is a function pointer and is callable via the Fn trait.
Method Calls and the Dot Operator
A method is a function attached to a specific type. You call it with the dot operator on a value of that type.
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect = Rectangle { width: 30, height: 50 };
let a = rect.area(); // method call expression
println!("Area: {a}");
}
The expression rect.area() does more than just call the function area with a hidden first argument. The dot operator performs automatic referencing and dereferencing so that the receiver (rect) matches what the method expects. Even though area declares a parameter &self, you do not have to write (&rect).area() — the compiler inserts the borrow for you. The same works for &mut self and for self that takes ownership.
This auto-referencing handles multiple levels of indirection transparently. If you have a &&Rectangle, calling .area() still works because the compiler will follow the deref chain until it finds a type that implements the method.
Method resolution follows a well‑defined order:
- Look for an inherent method on the receiver's concrete type.
- Walk through any
Dereftargets for that type and try step 1 again. - Search the trait implementations in scope, again following the
Derefchain at each step.
Method resolution can be order‑dependent:
If multiple methods with the same name exist on different traits in scope, the compiler may report an ambiguity error. You can use fully qualified syntax like TraitA::method(&value) to disambiguate when needed.
The return type of a method call is just the return type of the method. Like any other expression, you can use the result directly.
impl Rectangle {
fn scale(&mut self, factor: u32) {
self.width *= factor;
self.height *= factor;
}
}
let mut rect = Rectangle { width: 10, height: 20 };
rect.scale(2); // call returns ()
let scaled_area = rect.area(); // now 800
Even though scale returns nothing, the call rect.scale(2) is still an expression evaluating to (), so Rust's grammar remains uniform.
Associated Functions (No self)
Functions declared inside an impl block that do not take self as their first parameter are called associated functions. You call them with :: — the same path‑like syntax used for module items.
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
let sq = Rectangle::square(15);
The expression Rectangle::square(15) produces a Rectangle. Associated functions are often used as constructors or utility functions tied to a type. Because they do not receive a self argument, they cannot access instance‑specific data directly.
Consistent call syntax:
Whether you are calling a free function, a function pointer, an associated function, or a method, Rust always uses the postfix (args) syntax. This uniformity means once you learn how to call one kind of callable, you can call them all.
Closures Are Called the Same Way
Closures, which are anonymous functions that can capture their environment, also use () for calling. Under the hood a closure is a value that implements one of the Fn, FnMut, or FnOnce traits, each of which defines a call operator.
let add_closure = |a, b| a + b;
let sum = add_closure(3, 4); // closure call expression
From the perspective of an expression, a closure call is just another call — it evaluates its arguments, invokes the closure body, and yields the return value. You can pass closures to any function that expects a callable, like Iterator::map.
let doubled: Vec<_> = (1..5).map(|x| x * 2).collect();
// |x| x * 2 is called once per element by map
Common Mistakes When Calling
Missing Parentheses
A function or method name without () is the callable itself, not a call.
let f = greet; // f is now a function item, not the result of calling greet
let m = rect.area; // m is a function pointer, not the area
If you intended to call the function and store the result, you must append ().
Forgotten parentheses can compile silently:
If you assign a function item to a variable and then never use it, the compiler may only emit an unused warning rather than an error. The mistake can go unnoticed until the program behaves unexpectedly because a value is missing. Always double‑check zero‑argument calls.
Accidental Semicolon After a Call That Should Be Captured
Rust expressions become statements when you add a semicolon. A call with a trailing semicolon evaluates to () and discards the return value.
let result = {
greet("Rust"); // returns ()
// `result` would be (), not the greeting string
};
If you need the returned value, remove the semicolon so the call is the final expression of the block.
Confusing Method and Associated Function Syntax
Methods always use . on a value, and Rust automatically handles the reference. Associated functions use :: on the type and do not receive an instance.
let s = String::from("hello"); // associated function — no self
let len = s.len(); // method — self is &self
Trying to write s::len() or String::from(...) with a dot will produce a compiler error. The distinction is not just syntactic; it reflects whether the call needs instance data.
Forgetting That return Inside a Call Is Unrelated
A call expression always evaluates to a single value. Early returns inside the called function do not affect the expression's type — they just exit the function early with some value that becomes the result of the call.
fn maybe_fail(flag: bool) -> Option<i32> {
if flag { return None; }
Some(42)
}
let value = maybe_fail(false); // Some(42)
The call's type is Option<i32> regardless of which branch executes.
Beware of diverging calls:
A call to a diverging function (return type !, like panic!() or std::process::exit) never returns to the caller. In expression terms, such a call can appear anywhere because ! coerces to any type. For example, let x: i32 = panic!("crash"); compiles, but execution stops at the call.
Function and Method Calls as Building Blocks
Because every call is an expression, you can compose them in the middle of larger expressions without temporary variables.
let side = 12;
let square_area = Rectangle::square(side).area();
Here Rectangle::square(side) returns a Rectangle, and then .area() immediately calls a method on that result. This works because method calls chain naturally with the expression‑based design.
The expression-oriented nature encourages writing code as a pipeline of transformations. Combined with iterators and map, calls become the primary way you express logic.
let names = vec!["Alice", "Bob"];
let greetings: Vec<_> = names.iter().map(|n| greet(n)).collect();
// Each call to greet(n) is an expression inside the closure.
Summary
Function and method calls in Rust are ordinary expressions that produce values — even when returning (). The syntax is uniform across free functions, method calls, associated functions, and closures: a callable followed by (arguments). The dot operator for methods adds automatic referencing and dereferencing so you can call &self, &mut self, and self methods on any level of indirection without manual conversion.
A call's type is the declared return type, which enables you to compose calls freely within larger expressions. Understanding that a call is not a separate "statement" but a value‑producing expression helps you write concise, functional‑style Rust without extra intermediate bindings.