Ownership and Functions
How passing values to functions and returning them interacts with Rust ownership rules, with practical examples and common pitfalls
When you pass a variable to a function in Rust, you are making a decision about who gets to use that value afterward. Unlike languages where arguments are just temporary handles to shared data, Rust ties argument passing directly to its ownership system. The same rules that govern variable assignment—each value has exactly one owner, and ownership can move—apply when values cross function boundaries.
Passing Ownership to Functions
Passing a value to a function is ownership transfer in the same way that assigning one variable to another is. The moment a value enters the function parameter, the parameter becomes its new owner. If the type does not implement the Copy trait, the original variable can no longer be used.
fn consume_string(s: String) {
println!("Inside function: {}", s);
} // s goes out of scope here; the String's heap memory is freed
fn main() {
let greeting = String::from("hello");
consume_string(greeting);
// println!("{}", greeting); // ERROR: greeting has been moved
}
When consume_string(greeting) is called, the heap-allocated String that was owned by greeting moves into the parameter s. After the call, greeting is no longer valid. The compiler stops any attempt to read it. When consume_string finishes, s goes out of scope and Rust automatically calls drop on the String, releasing the memory. No double-free, no dangling pointer, no runtime check.
Compile-time error:
Trying to use a moved value is a hard compiler error. You will see a message like error[E0382]: borrow of moved value: greeting. The compiler prevents the bug before the program ever runs.
Types that implement Copy behave differently. When you pass an i32, a bool, or any other Copy type to a function, the value is bitwise-copied into the parameter. The original variable remains usable, because copying is cheap and the two copies are independent.
fn double(n: i32) -> i32 {
n * 2
}
fn main() {
let x = 5;
let result = double(x);
println!("x is still {}; result is {}", x, result); // Both are valid
}
This is not a move. x and the parameter n each hold their own 5. The function’s stack frame gets its own copy, and when the function returns, that copy is destroyed without affecting x.
A useful mental model: imagine a value that lives on the heap as a unique physical object, like a printed book. Passing it to a function is handing that exact book over. You no longer have it. A Copy type is more like a recipe card—handing someone a copy of the recipe doesn’t stop you from cooking the dish yourself.
Returning Ownership from Functions
A function can also hand ownership back to the caller through its return value. This is how you move a value into a function, transform it, and get the result—without the caller losing access permanently.
fn create_greeting(name: String) -> String {
let mut message = String::from("Hello, ");
message.push_str(&name);
message
} // Ownership of `message` moves to the caller
fn main() {
let name = String::from("Alice");
let greeting = create_greeting(name);
println!("{}", greeting);
}
Inside create_greeting, the local message owns the String it builds. The last expression message (no semicolon) returns the value and transfers ownership to the caller. The variable greeting in main becomes the new owner. The same ownership transfer applies: name moves into the function and is consumed.
A function can even accept ownership of a value, do something with it, and then return it. This pattern is common when you need to use a value inside a function but also want to keep using it afterward.
fn append_world(mut s: String) -> String {
s.push_str(" world");
s
}
fn main() {
let phrase = String::from("hello");
let phrase = append_world(phrase);
println!("{}", phrase); // hello world
}
The original phrase moves into append_world. The function appends text, then returns the now-modified String. By assigning the return value back to phrase (shadowing the old binding), the caller regains a usable value. The variable name hasn’t changed, but ownership has moved out and back in.
Ownership round-trip:
This "take and give back" pattern is a legitimate Rust idiom. You are not breaking the ownership system—you are temporarily transferring ownership and then claiming it again through the return value.
Returning Multiple Values with Ownership
Often a function needs to hand back ownership alongside some computed result. Rust’s tuple type makes this straightforward.
fn calculate_length(s: String) -> (String, usize) {
let len = s.len();
(s, len)
}
fn main() {
let text = String::from("hello");
let (text, length) = calculate_length(text);
println!("'{}' has {} characters", text, length);
}
The function takes ownership of the String, computes its length, and returns both the original String and the length in a tuple. The caller destructs the tuple, recovering the String under the same name and gaining the length. No cloning, no reference counting—just clean ownership transfer.
Don't forget to return what you need:
A beginner trap is writing a function that takes ownership of a String to inspect it, then not returning it. The caller loses the value. If you need the value after the function call, either return it or consider using a reference, which the next section covers.
When Ownership Transfer Becomes Cumbersome
As programs grow, moving ownership in and out of every function becomes noisy. If you call several functions that each need to read a String, you would have to pass ownership in, then return it from each one—or make clones. Cloning duplicates the heap data and gives each owner an independent copy, but it is explicit and not free.
fn greet(s: String) -> String {
println!("Hello, {}!", s);
s
}
fn main() {
let name = String::from("Bob");
let name = greet(name); // pass ownership and get it back
let name = greet(name); // do it again
println!("Final: {}", name);
}
This works, but the boilerplate of passing and returning the same value grows quickly. This is where Rust’s borrowing system becomes essential. Instead of transferring ownership, you can lend a reference to the function. The function borrows the value temporarily, leaving the original owner intact.
fn borrow_greet(s: &String) {
println!("Hello, {}!", s);
}
fn main() {
let name = String::from("Bob");
borrow_greet(&name);
borrow_greet(&name); // name is still valid
println!("Final: {}", name);
}
The & creates a reference. The function signature &String says “I only need to look at the value, I won’t take it.” The caller retains ownership and can reuse name without ceremony. References are the primary tool for avoiding excessive ownership transfers.
Common Mistakes
Attempting to use a moved value after a function call:
One of the most frequent errors beginners see is error[E0382]: use of moved value. It occurs when you pass a non-Copy value to a function and then try to use it in the same scope. The fix is either to return the value, use a reference, or clone the data if you truly need two independent owners.
Another subtle issue arises with the println! macro. It looks like a function call, but macros do not take ownership of their arguments. Passing a String to println!("{}", s) does not move s. This can create a false sense of security—you won’t get a move error from the macro, but a real function with the same signature would move the value.
fn main() {
let s = String::from("hello");
println!("{}", s); // OK: println! does not move
// take_ownership(s); // This would move
println!("{}", s); // Still valid
}
Understanding that macros and functions differ in this regard prevents confusion when you later write your own functions.
Summary
Functions and ownership interact through the same move semantics that apply to assignment. A function parameter becomes the new owner of any non-Copy value passed to it, and the original variable is invalidated. Returning a value transfers ownership back to the caller. These rules ensure that every heap allocation has a clear, single responsible owner at all times, eliminating an entire class of memory bugs without a garbage collector.
The patterns you’ve seen here—passing ownership in, returning it out, using tuples to carry extra data—are the raw building blocks. They work, but they often lead to noisy code. References solve that problem by letting functions borrow values without taking ownership.