You hand over the data, and it vanishes
You write a function that takes a String, processes it, and returns a result. You call the function, pass your variable, and the compiler rejects the next line where you try to print that same variable. It says the value was moved. You didn't copy it. You handed it over, and now you're holding an empty box.
This isn't a bug. This is Rust's core mechanic. In languages like Python or JavaScript, passing a variable usually creates a reference or increments a hidden counter. You can pass the same object to ten functions, and it all works. Rust refuses to hide that complexity. It forces you to declare exactly who owns the data at every moment. If you try to use data you no longer own, the compiler stops you. This rule eliminates entire classes of bugs, like double-free errors and use-after-free crashes, without needing a garbage collector.
One owner, one cleanup
Rust's ownership system rests on three rules. Every value has exactly one owner. When the owner goes out of scope, the value is dropped and its memory is reclaimed. You can access a value without taking ownership by borrowing it, but borrowing follows strict rules that prevent data races.
Think of ownership like a physical object. If you hand a book to a friend, you no longer hold the book. Your friend does. If you both try to shred the book when you're done, you get a mess. Rust enforces the rule: exactly one owner at a time. The owner is responsible for cleaning up. When the owner's scope ends, Rust calls the drop method, which frees the memory. No garbage collector pauses your program. No double-free errors corrupt your heap. Just strict accounting.
The minimal move
The most common way ownership changes hands is through assignment. When you assign a variable to another variable, ownership moves. The original variable becomes invalid.
fn main() {
// Create a String on the heap. s1 owns the heap data.
let s1 = String::from("hello");
// Move ownership to s2. s1 is now invalid.
let s2 = s1;
// s2 owns the data now. Printing works.
println!("{}", s2);
// This would fail. s1 no longer owns the data.
// println!("{}", s1);
}
Convention aside: String allocates memory on the heap. The variable holds a pointer, length, and capacity. Moving the variable moves that metadata. The heap data stays put; only the ownership token changes hands. This is efficient. Rust copies three words on the stack, not the entire string.
Trust the move. It's free and safe.
What happens under the hood
When you write let s2 = s1, Rust performs a bitwise copy of the String structure from s1 to s2. This structure contains the pointer to the heap, the length of the string, and the allocated capacity. After the copy, Rust marks s1 as invalid. The compiler tracks this state. If you try to use s1, you get E0382 (use of moved value).
When s2 goes out of scope, Rust calls drop on s2. The drop implementation frees the heap memory pointed to by the pointer. s1 never gets dropped because it's invalid. The compiler prevents you from dropping the same memory twice.
This behavior applies to any type that manages resources. Vec<T>, File, TcpStream, and Mutex<T> all move ownership. They all allocate or hold external resources. Moving ensures only one variable is responsible for cleanup.
The compiler tracks ownership like a hawk. If you lose track, it stops the build.
Scope controls the lifecycle
Ownership lives and dies with scope. A scope is the block of code where a variable is valid. Usually, that's the curly braces. When the closing brace is reached, Rust runs the drop method for any owned values in that scope. This reclaims memory.
fn main() {
// s is created here.
let s = String::from("inside scope");
// s is valid here.
println!("{}", s);
// Scope ends. s is dropped. Memory is freed.
}
// s is invalid here.
// println!("{}", s); // Error
You can force a drop early with the drop function. This is useful when you need to reuse a variable name or free memory before a long computation.
fn main() {
let data = String::from("large payload");
// Use data...
println!("{}", data);
// Drop data early to free memory.
drop(data);
// data is invalid now.
// println!("{}", data); // Error
}
Convention aside: When you intentionally drop a value to silence a warning, use let _ = value. This signals to readers that you considered the value and chose to discard it. It's cleaner than calling drop just to suppress a lint.
Drop early if you need the memory back. Otherwise, let the scope handle it.
Copy types break the pattern
Not everything moves. Integers, booleans, and floats implement the Copy trait. When you assign an i32, Rust duplicates the bits. The original variable stays valid.
fn main() {
// x is an i32. It implements Copy.
let x = 5;
// y gets a copy of the bits. x is still valid.
let y = x;
// Both work.
println!("x: {}, y: {}", x, y);
}
This is because these types have a known, fixed size on the stack. No heap allocation means no complex cleanup. The compiler knows it's safe to copy. You can mark your own types as Copy if they only contain Copy fields and don't manage resources.
#[derive(Copy, Clone)]
struct Point {
x: f64,
y: f64,
}
fn main() {
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1; // Copy, not move.
println!("{}, {}", p1.x, p2.x); // Both valid.
}
If your type holds a pointer, a file handle, or any resource, it cannot be Copy. The compiler enforces this. You can't accidentally copy a String and create a double-free.
Use Copy for simple data. Use move for resources. The compiler decides based on the trait.
Realistic flow: passing and returning
Functions take ownership by value. If a function parameter is a String, the caller must move a String into it. The function owns the value for the duration of the call. When the function returns, the value is dropped unless the function returns it.
/// Takes ownership of a String, processes it, returns the length.
fn process_data(data: String) -> usize {
// data owns the String here.
let len = data.len();
// data is dropped at the end of this function.
len
}
fn main() {
let my_data = String::from("important payload");
// Move my_data into process_data.
let size = process_data(my_data);
// my_data is gone. Cannot use it here.
// println!("{}", my_data); // Error
println!("Size: {}", size);
}
If the caller needs the value after the function returns, the function must return it. Ownership can be passed back.
/// Takes ownership, modifies the String, and returns it.
fn append_suffix(mut data: String) -> String {
// data is owned and mutable here.
data.push_str(" [processed]");
// Return ownership to the caller.
data
}
fn main() {
let mut payload = String::from("data");
// Move payload in, get it back out.
payload = append_suffix(payload);
// payload is valid again.
println!("{}", payload);
}
This pattern is common in builder APIs and transformation pipelines. You pass ownership in, the function does work, and you get ownership back. The compiler ensures you can't use the value while the function holds it.
Ownership isn't a restriction. It's a contract. Define who owns what, and the rest falls into place.
Pitfalls and compiler errors
The compiler rejects moved usage with E0382 (use of moved value). This happens when you assign a variable, pass it to a function, or return it, and then try to use the original variable. The fix is usually to clone the value if you need a duplicate, or borrow it if you only need to read it.
You might see E0507 (cannot move out of borrowed content) if you try to extract data from a reference. A reference is a view, not the data. You can't steal the data through a window.
fn main() {
let s = String::from("hello");
// &s is a reference. It doesn't own the String.
let r = &s;
// This fails. You can't move the String out of a reference.
// let t = *r; // Error E0507
// You can copy the reference.
let r2 = r;
// Both references are valid.
println!("{}, {}", r, r2);
}
Another common error is E0502 (cannot borrow as mutable because it is also borrowed as immutable). This happens when you try to mutate data while an immutable reference exists. Rust prevents data races at compile time. You can't have a mutable reference and any other reference at the same time.
Read the error code. E0382 tells you exactly which variable was moved. Fix the flow, don't fight the compiler.
Choosing the right transfer
Rust gives you precise control over how data moves. Pick the right tool for the job.
Use move when the function takes full responsibility for the value and the caller no longer needs it. This is the default for owned types. It's efficient and safe.
Use borrowing when you need to access the data without taking ownership, allowing the original owner to keep using it. Pass &T for read-only access or &mut T for mutation. Borrowing has zero cost and keeps ownership clear.
Use clone when you need an independent duplicate of the data and are willing to pay the cost of allocation and copying. Cloning creates a new owner. Use it sparingly in performance-critical code.
Use copy types like integers, booleans, and floats for small, stack-only values where the compiler automatically duplicates the bits. Derive Copy for your own simple structs.
Reach for &str instead of String when you only need to read text and want to avoid allocation. &str accepts both string literals and String slices, making your API more flexible.
Convention aside: Prefer &str in function parameters when possible. It reduces allocations and allows callers to pass literals without wrapping them in String::from. Reserve String for parameters that need to take ownership or mutate the text.
Ownership is the foundation. Master moves and borrows, and the borrow checker becomes an ally, not an enemy.