The variable didn't vanish. It moved.
You write a function to log a user's name. You pass the name string to the function. The function runs. You try to print the name again to confirm it worked. The compiler screams.
error[E0382]: use of moved value: 'name'
You didn't delete the variable. You didn't even touch it after the call. Why does Rust think the name vanished?
The compiler isn't being difficult. It's saving you from a crash that would happen three hours later in production.
Ownership is physical
Rust treats most variables like physical objects. If you hand a book to a friend, you no longer hold the book. You can't read the next page while your friend has it. The compiler enforces this rule to prevent two people from trying to edit the same page at once, or one person from tearing up the book while the other is still reading.
When you pass a value to a function, Rust assumes you are handing over the object. The function now owns it. You are empty-handed. This model eliminates entire classes of bugs. In languages with garbage collection, the runtime tracks references to decide when memory is safe to free. In C, you manage pointers manually and risk dangling pointers. Rust gives you the safety of garbage collection with the performance of manual management. The trade-off is that the compiler checks ownership at compile time. If the rules aren't followed, the code doesn't run.
Think of references as a loan. The borrower can look at the book, but they can't keep it forever.
Minimal example: the move and the fix
Here is the code that triggers the error, followed by the fix.
/// Demonstrates the move error.
fn main() {
let data = String::from("important data");
// This moves `data` into the function.
// Ownership transfers. `data` is gone in `main`.
process_data(data);
// Error: E0382 use of moved value.
// The compiler knows `data` was moved and is invalid.
// println!("{}", data);
}
/// Accepts ownership of a String.
fn process_data(s: String) {
println!("Processing: {}", s);
// `s` is dropped here. Memory is freed.
}
The fix is to pass a reference instead of the value. You add an ampersand to the call and update the function signature to accept a reference.
/// Demonstrates borrowing to keep ownership.
fn main() {
let data = String::from("important data");
// Pass a reference. `data` stays in `main`.
// The function gets a temporary view.
process_data(&data);
// `data` is still valid. You can use it again.
println!("Still have: {}", data);
}
/// Borrows the String. Does not take ownership.
fn process_data(s: &String) {
println!("Processing: {}", s);
// `s` is a reference. Nothing is dropped.
}
Add the ampersand. Update the signature. Keep the data where it belongs.
What happens under the hood
When you write process_data(data), the compiler sees the function signature expects a String. It checks your variable data. It sees data is a String. It performs a move. The ownership token transfers from main to process_data. The variable data in main is now invalid. The compiler marks it as "moved." If you try to use it, the compiler stops you. This prevents use-after-free bugs. If process_data freed the memory, accessing data later would crash the program. Rust catches this at compile time.
When you write &data, you create a reference. The type changes from String to &String. The function signature must match. You update fn process_data(s: &String). Now the function borrows the value. It gets a temporary view. Ownership stays with main. When the function returns, the borrow ends. main still owns data.
The compiler tracks the lifetime of every reference. It ensures the reference never outlives the data it points to. You don't need to manage this manually. The borrow checker does it for you. If you try to return a reference to a local variable, the compiler rejects it. Local variables are dropped when the function returns. A reference to them would dangle. The compiler prevents this.
Borrowing lets you share data without copying. It's the backbone of efficient Rust code.
Realistic scenario: sharing configuration
In real code, you often have a configuration object that multiple parts of your program need to read. You don't want to copy the config every time. You want to pass it around efficiently.
/// Configuration for an application.
struct Config {
name: String,
debug: bool,
}
/// Initializes logging based on config.
fn init_logging(config: &Config) {
if config.debug {
println!("Debug mode enabled");
}
}
/// Starts the server using config.
fn start_server(config: &Config) {
println!("Server starting as {}", config.name);
}
fn main() {
let config = Config {
name: String::from("my-app"),
debug: true,
};
// Borrow config for logging.
init_logging(&config);
// Borrow config for server.
start_server(&config);
// Config is still valid. You can inspect it.
println!("Config name: {}", config.name);
}
Convention aside: In Rust, function parameters that take string references usually prefer &str over &String. &str is a slice that can point to a String or a string literal. It's more flexible. The compiler often suggests this automatically. You can write fn process(s: &str) and call it with &my_string or "literal". This is a community standard for API design.
Use &str for string parameters. It makes your API easier to use.
Pitfalls and compiler errors
You will hit these errors when working with moves and borrows.
error[E0382]: use of moved value. This is the classic. You used a variable after passing it by value. The fix is to pass a reference or clone the value.
error[E0308]: mismatched types. You passed &data but the function expects String. Or vice versa. The compiler tells you the types don't match. Check your function signature and your call site.
error[E0507]: cannot move out of borrowed content. This happens when you try to move a field out of a struct. You can't take a field out of a struct unless the whole struct is moved or you use a reference. If you have a struct and try to pass a field by value, the compiler blocks you. You need to borrow the field or clone it.
error[E0502]: cannot borrow as mutable because it is also borrowed as immutable. You can't have a mutable borrow and an immutable borrow at the same time. If you pass &data to one function and &mut data to another, the compiler rejects it. This prevents data races. You must drop the immutable borrow before starting the mutable one.
Read the error code. E0382 is a friend. It points exactly to the line where ownership broke.
When you actually need a copy
Sometimes you need two independent values. If you modify one, the other shouldn't change. References don't do this. A reference points to the same data. You need a clone.
Call .clone() on the value. This creates a deep copy. The memory is duplicated. Both owners have their own data.
/// Demonstrates cloning for independent copies.
fn main() {
let original = String::from("hello");
// Create a deep copy.
let copy = original.clone();
// Modify the copy. Original is untouched.
let mut copy = copy;
copy.push_str(" world");
println!("Original: {}", original); // "hello"
println!("Copy: {}", copy); // "hello world"
}
Convention aside: data.clone() is the standard way to clone. It's clear and readable. Some codebases use Clone::clone(&data), but the method syntax is preferred. It signals intent clearly.
Some types implement the Copy trait. i32, bool, f64, and tuples of copy types implement Copy. These types copy automatically when passed. They never move. The compiler inserts a copy behind the scenes. You don't need & for these. If you try to add & to a Copy type, the code still works, but it's unnecessary. The compiler might warn you. Use Copy types for small, stack-based values. Use references for large, heap-based values like String or Vec.
Clone when you need independence. Borrow when you need efficiency. The choice is yours, but the compiler enforces the consequences.
Decision: when to use what
Use references (&T) when the function only needs to read the data and the caller must keep using the value afterward.
Use mutable references (&mut T) when the function needs to modify the data in place and you need the changes to persist after the function returns.
Use ownership transfer (T) when the function takes responsibility for the data, such as storing it in a collection or freeing it immediately.
Use Clone when you genuinely need two independent copies of the data, and the cost of cloning is acceptable.
Use Copy types (like i32 or bool) when you want values to behave like primitives and avoid move semantics entirely.
Choose the smallest permission the function needs. Ownership is a heavy commitment. Borrowing is a light touch.