The variable that vanished
You write a function to sanitize a user's bio string. You pass the String into the function. Immediately after, you try to print the original variable to the console. The compiler rejects the code with a hard error. It feels like the function stole the data. It did. Rust calls this a move.
The error message points to the line where you try to use the variable after passing it. It says the value was moved. To a programmer coming from Python or JavaScript, this looks like a bug. The variable is still in scope. It should still hold the data. Rust disagrees. The data is gone from that variable. Ownership transferred to the function.
Ownership is a single handle
Rust treats data like physical objects with a single owner. A String is a complex object. It holds a pointer to a chunk of memory on the heap where the actual characters live. The variable on the stack is just a handle. It contains the pointer, the length, and the capacity.
When you assign let s2 = s1, Rust doesn't duplicate the heap data. Duplicating a long string would be expensive. It would allocate new memory and copy every byte. Instead, Rust hands the handle to s2. s1 is now empty. It has no handle. It points to nothing.
If s1 could still use the data, and s2 also used it, both would try to free the memory when they go out of scope. That's a double-free crash. Rust prevents the crash by invalidating s1 at the moment the move happens. The borrow checker enforces this rule. You cannot use a value after you've moved it.
Minimal move
/// Demonstrates ownership transfer via move.
fn main() {
// s1 owns a String allocation on the heap.
// The stack variable holds the pointer, length, and capacity.
let s1 = String::from("hello");
// The handle moves to s2. s1 is invalidated.
// Rust prevents double-free by making s1 unusable.
let s2 = s1;
// s2 is the sole owner. Access is safe.
println!("{s2}");
// Uncommenting this line triggers E0382: use of moved value.
// println!("{s1}");
}
What happens under the hood
At compile time, the borrow checker tracks ownership. It sees let s2 = s1. It marks s1 as moved. Any subsequent use of s1 fails the check. The compiler generates the error before the program runs.
At runtime, there is no overhead. The move is just a pointer copy on the stack. It's as fast as copying an integer. The magic is entirely in the compiler's bookkeeping. The runtime doesn't know about "moves." It just sees memory operations. The safety guarantee comes from the fact that the compiler refused to generate code that would use s1 after the move.
Treat the move as a transfer of responsibility. Once you move a value, the new owner is responsible for cleaning it up.
Why Rust moves instead of copying
The move semantics exist to manage memory without a garbage collector. When a variable goes out of scope, Rust calls its destructor. For a String, the destructor frees the heap memory. If multiple variables could own the same String, the destructor would run multiple times. The first run frees the memory. The second run tries to free memory that's already gone. The program crashes or corrupts data.
By ensuring exactly one owner, Rust guarantees the destructor runs exactly once. The memory is freed exactly once. This is the core of Rust's safety. Moves are the mechanism that enforces single ownership.
When types copy instead of move
Not all types move. Simple types like i32, bool, and f64 implement the Copy trait. When you assign a Copy type, Rust duplicates the bits. It doesn't move. Both variables hold independent copies.
/// Demonstrates Copy behavior for primitive types.
fn main() {
// x holds an integer on the stack.
let x = 42;
// y gets a bitwise copy of x.
// x remains valid because integers are Copy.
let y = x;
// Both x and y are usable.
println!("x = {x}, y = {y}");
}
The Copy trait is a marker. It tells the compiler that the type can be duplicated by copying bits. If a type implements Copy, assignments and function arguments copy the value instead of moving it. This makes the API smoother. No one wants to move an integer.
Convention aside: if you define a struct that contains only Copy fields, derive Copy and Clone. This allows the struct to behave like a primitive. It makes the type easier to use in collections and function arguments.
/// A trivial struct that can be copied.
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 10, y: 20 };
let p2 = p1; // Copy, not move.
println!("{p1:?}"); // p1 is still valid.
}
The Copy and Drop conflict
A type cannot implement both Copy and Drop. These traits are mutually exclusive. The reason is logical. Copy means you can create multiple independent owners by duplicating bits. Drop means the type has a destructor that runs when an owner goes out of scope.
If a type were both Copy and Drop, you could copy it to create two owners. When the first owner goes out of scope, Drop runs and frees resources. The second owner still holds a copy of the handle. It now points to freed resources. That's a dangling pointer. Rust forbids this combination to prevent undefined behavior.
If your type needs custom cleanup, it must implement Drop. It cannot be Copy. If your type is trivial and doesn't need cleanup, it can be Copy. You have to choose one path.
Partial moves in structs
Structs move field by field. You can move one field of a struct while leaving the others behind. This is called a partial move. It's a powerful feature, but it requires care.
/// Demonstrates partial moves in a struct.
struct User {
name: String,
age: i32,
}
fn main() {
// u owns the struct.
let u = User {
name: String::from("Alice"),
age: 30,
};
// Moving a field moves that field.
// name is now owned by the local variable.
let name = u.name;
// u.age is still valid. u.name is moved.
println!("Age: {}", u.age);
// u is partially moved. You cannot use u.name.
// println!("{}", u.name); // Error: value borrowed here after partial move.
// u cannot be dropped normally because name was moved.
// drop(u); // Error: use of partially moved value.
}
When you move a field, the struct becomes partially moved. You can still use the fields that weren't moved. You cannot use the struct as a whole. The compiler prevents you from dropping the struct because the destructor would try to drop the moved field, causing a double-free. You must drop the moved fields separately, or use the whole struct before moving any fields.
Partial moves are useful when you want to extract data from a struct without copying everything. They are also a source of confusion. If you see an error about a partially moved value, check if you moved a field earlier in the scope.
Pitfalls and compiler errors
The compiler error E0382 is the guardian of moves. It says "use of moved value." You see this when you try to use a variable after passing it to a function or assigning it to another variable. The fix is usually to clone the value if you need to keep it, or to restructure the code so the move happens at the end of the variable's life.
Another common error involves partial moves. If you move a field, you cannot drop the struct. The compiler will complain about using a partially moved value. The fix is to avoid partial moves unless you need them. If you need to extract a field, consider cloning the field or using references.
Convention aside: if you move a value and don't use the new variable, the compiler warns about unused variables. Use let _ = value to discard a result. This signals to readers that you considered the value and chose to drop it. It suppresses the warning and makes the intent clear.
Decision matrix
Use moves when the receiving code becomes the sole owner and the sender is done with the data. This avoids allocation overhead and clarifies responsibility. The receiver takes over the memory management.
Use clones when the sender needs to keep the data and the receiver also needs ownership. Cloning duplicates the heap content, so measure the cost if the data is large. Cloning is the escape hatch, not the default.
Use references when the receiver only needs to read or modify the data temporarily. References avoid moving ownership and keep the original variable valid. Borrowing is the standard way to share data without copying.
Use Copy types for small, trivially duplicable data like integers, booleans, and pointers. Derive Copy on structs that contain only Copy fields to enable implicit duplication. This makes the API smoother and avoids unnecessary moves for simple data.
Moves are the default for heap data. References are the default for sharing. Clones are the exception. Trust the borrow checker. It usually has a point.