The handoff
You write a function to fetch user data from an API, parse the JSON, and hand the result back to your main loop. In Python, you just return data. In Rust, you try the same thing, but the compiler forces you to think about who owns the data after the function finishes. Returning ownership is the mechanism that lets a function create a value and pass responsibility for that value to the caller. The function gives up the value, and the caller becomes the new owner.
This isn't just syntax. It's how Rust prevents double-free bugs and dangling pointers without a garbage collector. When you return a value, you are making a contract: the function stops using the data, and the caller takes over. The compiler enforces this contract at compile time. If you try to use the value after returning it, or if you try to return a reference to data that dies inside the function, the code won't compile.
Mechanics: moving pointers, not data
Returning a value moves ownership, but it doesn't necessarily copy the underlying data. For heap-allocated types like String or Vec<T>, the value on the stack is just a pointer, a length, and a capacity. Returning the value copies those three words to the caller's stack frame. The heap data stays exactly where it is. No bytes are shuffled around in memory.
This makes returning large strings or vectors cheap. You are moving a small stack structure, not cloning megabytes of heap data. The move is a pointer transfer. The caller gets the pointer, the function loses it, and the heap allocation remains valid because the caller now owns it.
When the caller eventually drops the value, the destructor runs and frees the heap memory. Because ownership transferred cleanly, only one owner exists at any time. The memory is freed exactly once. This is the core safety guarantee.
Minimal example
/// Creates a new String and returns ownership to the caller.
fn create_message() -> String {
// Allocate a String on the heap.
// `msg` owns the heap data.
let msg = String::from("Hello from the function");
// Return the value. This moves `msg` out of the function.
// The caller becomes the new owner.
// The function can no longer use `msg`.
msg
}
fn main() {
// `create_message` moves the String out.
// `s` now owns the heap data.
let s = create_message();
// We can use `s` here because we own it.
println!("{}", s);
// When `s` goes out of scope, the String is dropped.
// The heap memory is freed.
}
The function create_message allocates a String. The variable msg owns it. The return expression msg moves ownership to the caller. Inside main, s receives the ownership. The heap data is accessible through s. When s ends, the data is cleaned up.
Return the value, not a reference, when the data is created inside the function. The caller needs ownership to keep the data alive.
Realistic example: processing a struct
Real code often involves transforming data. You take ownership of an input, process it, and return a new value. The caller hands in the data and gets a result back.
/// Represents a user with a name and email.
struct User {
name: String,
email: String,
}
/// Sanitizes a user by trimming whitespace and lowercasing the email.
/// Takes ownership of the user and returns a new sanitized user.
fn sanitize_user(user: User) -> User {
// `user` is moved into this function.
// The caller can no longer use the original `user`.
// We extract the fields. This moves them out of the struct.
let name = user.name.trim().to_string();
let email = user.email.trim().to_lowercase();
// Construct a new User struct.
// This moves `name` and `email` into the struct.
User { name, email }
}
fn main() {
// Create a user with messy data.
let raw_user = User {
name: String::from(" Alice "),
email: String::from("ALICE@EXAMPLE.COM"),
};
// Pass ownership to `sanitize_user`.
// `raw_user` is moved.
let clean_user = sanitize_user(raw_user);
// `raw_user` is invalid here.
// This line would cause E0382 (use of moved value).
// println!("{}", raw_user.name);
// `clean_user` owns the sanitized data.
println!("Name: {}, Email: {}", clean_user.name, clean_user.email);
}
The function sanitize_user takes a User by value. This moves the struct into the function. The fields name and email are moved out of the struct and processed. New strings are created and moved into a new User struct. The new struct is returned, moving ownership back to the caller. The original raw_user is gone. The caller now holds clean_user.
Trust the move. If the compiler says the value is moved, it is moved. You cannot use the variable again.
Copy types: when return copies instead of moves
Not all types move when returned. Types that implement the Copy trait are duplicated instead of moved. Primitive integers, booleans, and floats implement Copy. When you return an i32, the compiler generates code to duplicate the value. The function keeps its copy, and the caller gets a copy.
/// Returns a computed integer.
fn compute_id() -> i32 {
let id = 42;
// `id` is copied to the caller.
// `id` remains usable inside the function if needed.
id
}
fn main() {
let result = compute_id();
// `result` is a copy of the value.
println!("ID: {}", result);
}
The return behavior depends on the type. If the type implements Copy, the value is duplicated. If it does not, the value is moved. You can check the documentation for a type to see if it implements Copy. Most heap-allocated types do not implement Copy because duplicating heap data is expensive and requires deep cloning.
If the type implements Copy, the compiler duplicates the value. No move happens.
Pitfalls: dangling references and moved values
The most common mistake is trying to return a reference to a value created inside the function. Local variables are dropped when the function returns. A reference to a local variable would point to freed memory. The compiler catches this immediately.
// BAD CODE
fn bad_return() -> &String {
let s = String::from("temporary");
&s // Error: `s` is dropped at end of function.
}
The compiler rejects this with an error like "borrowed value does not live long enough" or E0515. The reference &s tries to escape the function, but s dies when the function ends. The reference would dangle. The fix is to return the String itself, not a reference. Return the value so the caller owns it.
Another pitfall is trying to return a value that has already been moved. If you move a value into a struct or another variable, you can't return it.
// BAD CODE
fn bad_move() -> String {
let s = String::from("data");
let _box = Box::new(s); // `s` is moved into the Box.
s // Error E0382: use of moved value.
}
The variable s is moved into Box::new. It is no longer valid. Trying to return s fails with E0382. You must return the Box or the value you actually own.
Don't return a reference to a local variable. The data dies when the function ends. Return the value instead.
Returning multiple values with tuples
Functions can return multiple owned values using tuples. The tuple groups the values together and moves them all to the caller.
/// Returns a name and an age.
fn get_user_info() -> (String, u32) {
let name = String::from("Bob");
let age = 30;
// Return a tuple containing both values.
// Both values are moved to the caller.
(name, age)
}
fn main() {
// Destructure the tuple.
// `name` and `age` are moved out of the tuple.
let (name, age) = get_user_info();
println!("{} is {} years old", name, age);
}
The tuple owns both values. Returning the tuple moves both values to the caller. The caller can destructure the tuple to get individual variables. This is a clean way to return multiple results without creating a custom struct.
Treat the return type as a contract. If you return a tuple, you hand over ownership of all contained values.
Convention asides
Community conventions shape how Rust code looks. When returning a String from a literal, use String::from("text") rather than "text".to_string(). Both work, but String::from is the standard way to create a String from a literal. It signals intent clearly.
When you call a function that returns a value you don't need, assign it to _ to discard it explicitly. let _ = create_message();. This tells readers you considered the return value and chose to drop it. It also suppresses warnings about unused results.
Keep unsafe blocks out of return logic unless you are implementing low-level abstractions. Returning values is safe by default. The compiler handles the moves. You don't need unsafe to return a String or a struct.
Convention is explicit. Use String::from for literals, and let _ = to discard results you don't need.
Decision matrix
Use a return type of T when you create a new value inside the function and want the caller to own it. The value moves out of the function scope.
Use a return type of T when you take ownership of an argument, transform it, and pass the result back. The caller hands in the value and gets a new one out.
Use a return type of &T when the data lives longer than the function call, such as when you receive a reference with a lifetime and return a slice or reference into that data. The function does not create the data; it only points to existing data.
Use Result<T, E> or Option<T> when the operation might fail or produce no value. The caller must handle the error or absence before accessing the owned value.
Use Box<T> when returning a trait object or a large value where you want to abstract the concrete type or avoid copying a huge struct. The box moves the heap pointer, not the data.
Use a tuple return type (T, U) when you need to return multiple owned values of different types without defining a dedicated struct. The tuple moves all values to the caller.