The Vanishing Variable
You write a function to process a user's name. In Python or JavaScript, you pass the string to the function, the function prints it, and you can still log the name back in main. The variable survives the call. In Rust, you pass the String, the function prints it, and suddenly the compiler rejects your code when you try to log the name again. The variable isn't just "used"; it's gone.
This isn't a bug. It's the core mechanic of Rust's memory safety. When you pass a value to a function, you are moving ownership. The function becomes the new owner. The original variable is invalidated. If you need to keep using the value, you must pass a reference instead.
Ownership is a Transfer, Not a Peek
Rust's ownership system enforces a single rule: every value has exactly one owner. When the owner goes out of scope, the value is dropped. Passing a value to a function transfers that ownership. The function now holds the deed. The caller no longer has rights to the data.
Think of ownership like a signed contract for a unique item. When you pass a String to a function, you are signing over the deed. The function now owns the house. You can't live there anymore. If you want to visit the house without giving up ownership, you hand over a map. The map points to the house, but the deed stays with you. In Rust, the map is a reference (&T).
This transfer prevents double-free errors. If both the caller and the function thought they owned the String, they would both try to free the heap memory when their scopes ended. The second free would corrupt memory. Rust eliminates this class of bugs by design. The compiler tracks ownership statically. If you try to use a moved value, the code won't compile.
/// Consumes a message and prints it.
/// Takes ownership of the `String`.
fn consume_message(msg: String) {
// The function now owns `msg`.
// It can read, modify, or store the value.
println!("Processing: {}", msg);
// `msg` is dropped here when the function returns.
// The heap memory is freed.
}
fn main() {
let data = String::from("Secret payload");
// Ownership moves from `data` to `consume_message`.
consume_message(data);
// `data` has been moved. The compiler rejects this line.
// Error E0382: use of moved value.
// println!("{}", data);
}
The compiler enforces single ownership to guarantee memory is freed exactly once. Trust the borrow checker. It usually has a point.
Borrowing: The Map to the House
Most functions don't need ownership. They just need to read the data. If you pass ownership for every read, you force the caller to create new values or clone data constantly. That's inefficient and awkward.
Rust solves this with borrowing. A reference (&T) lets a function access data without taking ownership. The reference is a pointer to the value. The compiler tracks the lifetime of the reference to ensure it doesn't outlive the data it points to.
When you pass a reference, the caller retains ownership. The function gets a temporary lease. Once the function returns, the lease expires. The caller can continue using the value, pass it to other functions, or modify it.
/// Calculates the length of a string.
/// Borrows the string without taking ownership.
fn calculate_length(s: &str) -> usize {
// `s` is a reference. We can read it, but we can't free it.
// The caller still owns the underlying data.
s.len()
}
fn main() {
let message = String::from("Hello, Rust!");
// Pass a reference. `message` stays in `main`.
let len = calculate_length(&message);
// `message` is still valid. We can use it again.
println!("Length: {}, Text: {}", len, message);
}
Community convention dictates that functions should take &str instead of &String when they only need to read text. &String forces the caller to have a String, but &str accepts both String and string literals. The compiler handles the coercion automatically. This makes your API more flexible. Reach for &str by default.
Mutable Borrowing: Exclusive Access
Sometimes a function needs to modify data in place. Rust allows this with mutable references (&mut T). A mutable reference gives the function exclusive access to the value. No other references can exist while the mutable reference is active.
This rule prevents data races and aliasing issues. If multiple parts of the code could modify the same data simultaneously, the result would be unpredictable. Rust forces you to choose: either many readers, or one writer. Never both.
/// Appends a suffix to the string in place.
/// Requires exclusive access to modify the data.
fn append_suffix(s: &mut String) {
// We have exclusive access. We can modify `s`.
// No other references to `s` can exist right now.
s.push_str("_processed");
}
fn main() {
let mut msg = String::from("hello");
// Pass a mutable reference.
append_suffix(&mut msg);
// `msg` is modified. The changes are visible here.
println!("{}", msg); // "hello_processed"
}
Mutable borrows are exclusive. If you hold the pen, no one else can read the page.
The Copy Shortcut
Not all types move when passed. Primitive types like i32, bool, f64, and char implement the Copy trait. When you pass a Copy type, the compiler generates a bit-for-bit copy on the stack. The original value remains valid.
This is an optimization. Copying a few bytes is cheap. The compiler knows these types don't manage heap memory, so there's no risk of double-free. It skips the move logic and treats the pass as a copy.
fn double(x: i32) -> i32 {
// `x` is copied. The original in `main` is untouched.
x * 2
}
fn main() {
let num = 5;
let result = double(num);
// `num` is still valid. `i32` implements `Copy`.
println!("Original: {}, Result: {}", num, result);
}
String does not implement Copy. It manages heap memory. Copying a String would require allocating new heap memory and copying the contents, which is expensive. The compiler forces you to be explicit. If you want a copy, call .clone(). If you want to share, use a reference.
If the type implements Copy, the move is a lie. It's a copy. The compiler handles the distinction automatically.
Pitfalls and Compiler Signals
Rust's ownership rules catch mistakes at compile time. The error messages point directly to the violation. Learning to read them saves hours of debugging.
The most common error is E0382: use of moved value. This happens when you try to use a variable after passing it to a function that takes ownership. The compiler tells you exactly where the move occurred and where the invalid use happened.
Another frequent error is E0502: cannot borrow as mutable because it is also borrowed as immutable. This occurs when you try to create a mutable reference while an immutable reference is still active. Rust forbids this to prevent data races. You must drop the immutable reference before creating the mutable one.
Error E0596: cannot borrow as mutable, as it is not declared mutable. This happens when you try to pass &mut value but value was declared with let instead of let mut. The variable itself must be mutable to allow mutable borrows.
Read the error code. E0382 means you moved something. E0502 means you're borrowing while someone else holds the key. E0596 means you forgot mut. The compiler is your ally. Fix the signature or the declaration, and the code will compile.
Decision: Choosing the Right Argument
Function signatures define the contract between caller and callee. The argument type signals intent and constraints. Choose the type that matches what the function actually needs.
Use ownership transfer when the function takes responsibility for the data, such as storing it in a struct or consuming it to produce a result. Use immutable references when the function reads data without changing it, allowing the caller to keep using the value. Use mutable references when the function modifies data in place, giving the caller a single point of mutation. Pass by value for types that implement Copy, like i32 or bool, because the compiler generates a cheap stack copy and the original remains valid.
Match the signature to the intent. If you don't need the value, don't take it.