When assignment stops working the way you expect
You write a function that takes a String. You pass it in. The function runs. You try to print the original variable afterward. The compiler yells at you. You add the move keyword to a closure and suddenly everything compiles, but you are not sure what actually changed. Meanwhile, passing an i32 works without any fuss. The difference is not magic. It is a deliberate split in how Rust handles data transfer versus data duplication.
The two camps of assignment
Rust divides every type into two categories based on what happens when you assign it to a new variable or pass it to a function. Types that implement the Copy trait duplicate their bits. Types that do not implement Copy transfer ownership. The move keyword is an explicit signal to the compiler that you want that transfer to happen, even when the default behavior would otherwise borrow.
Think of a physical key versus a photocopy of a key. If you hand someone the original key, you no longer have it. They are now responsible for it. That is a move. If you hand someone a photocopy, you still have the original. Both keys work. That is a copy. Rust defaults to the original key model for anything that manages resources, like heap memory, file handles, or network sockets. It uses the photocopy model only for simple, fixed-size values that live entirely on the stack.
The language makes this split because memory safety depends on knowing exactly who is responsible for cleanup. If two variables owned the same heap allocation, Rust would have to guess which one should free the memory when it goes out of scope. Guessing leads to double-free vulnerabilities. Rust refuses to guess. It forces you to choose duplication or transfer.
Minimal example
fn main() {
// i32 implements Copy. Assignment duplicates the 32 bits on the stack.
let a = 42;
let b = a;
// Both variables remain valid because the bits were copied, not moved.
println!("a is still usable: {}", a);
// String does not implement Copy. Assignment transfers the heap pointer.
let s1 = String::from("original");
let s2 = s1;
// s1 is now invalid. The compiler marks it as uninitialized.
// println!("{}", s1); // This would fail to compile
println!("s2 owns the string: {}", s2);
}
What the compiler actually does
When the compiler sees let b = a;, it checks the type of a. Since i32 implements Copy, the compiler generates machine code that reads the 32 bits from a and writes them to b. Both variables remain valid. The original a is untouched. The assignment operator behaves exactly like a bitwise copy.
When the compiler sees let s2 = s1;, it checks String. String holds a pointer to heap memory, a length, and a capacity. Copying those three numbers would create two variables pointing to the same heap allocation. If both went out of scope, Rust would try to free the same memory twice. To prevent it, Rust invalidates s1 after the assignment. The ownership moves to s2. The compiler enforces this by marking s1 as uninitialized. Any attempt to use s1 afterward triggers a compile-time error.
The Copy trait is a marker trait. It contains no methods. Its only job is to tell the compiler, "This type is safe to duplicate with a simple bitwise copy." The compiler uses this marker to decide whether to generate a copy instruction or to invalidate the source variable. If a type manages external resources, it cannot implement Copy. The compiler will reject the trait implementation if the type contains any non-Copy fields.
The closure capture trap
Closures introduce a layer of indirection that catches most beginners. By default, closures capture variables by reference. They borrow what they need. This saves allocations and keeps the original variables usable after the closure runs. The move keyword overrides this default. It forces the closure to take ownership of everything it captures.
This distinction matters when the closure outlives the scope where it was created. A borrowed reference is only valid as long as the referenced data lives. If the data is dropped, the reference dangles. The move keyword eliminates dangling references by transferring ownership into the closure. The closure becomes the sole owner. It cleans up the data when it finishes.
Convention note: The community treats move as a boundary marker. You add it at the edge of a closure to say, "Everything inside this block now belongs to this block." Keep the boundary clear. Do not sprinkle move everywhere. Use it only when ownership must cross a lifetime boundary.
Realistic example: crossing thread boundaries
use std::thread;
fn main() {
// The closure captures `data` by value because of `move`.
// This transfers ownership into the new thread.
let data = String::from("thread payload");
let handle = thread::spawn(move || {
// The thread now owns `data`. It will clean it up when finished.
println!("Thread received: {}", data);
});
// The main thread can continue. `data` is no longer accessible here.
// handle.join().unwrap();
}
Without move, the closure would try to borrow data. The spawned thread could outlive main, leaving the thread holding a reference to memory that main already cleaned up. The move keyword guarantees the thread owns its data. The compiler knows the memory will stay valid for the thread's entire lifetime.
Pitfalls and compiler errors
The most common tripwire is trying to use a variable after it has been moved. The compiler rejects this with E0382 (use of moved value). You will see this when you pass a Vec or String to a function, then try to read it again.
fn take_ownership(s: String) {
// s is owned here. It will be dropped when this function returns.
}
fn main() {
let msg = String::from("important");
take_ownership(msg);
// println!("{}", msg); // E0382: use of moved value `msg`
}
Another pitfall involves Copy types that look like they should move. Structs containing only Copy fields automatically implement Copy if you add the #[derive(Copy, Clone)] attribute. If you forget the derive, the struct defaults to move semantics. You will get E0382 even though the struct only holds integers. The fix is explicit: derive the traits or clone the struct manually.
Convention note: Always derive Clone alongside Copy. The Copy trait requires Clone in its definition. Writing #[derive(Copy, Clone)] is the standard pattern. The order matters for readability, but the compiler enforces the dependency.
Beware of Clone versus Copy. Clone is a method you call. Copy is a trait that changes assignment semantics. A type can implement Clone without implementing Copy. String implements Clone. Calling s.clone() creates a deep copy of the heap data. Assignment without .clone() still moves. Do not confuse the two. Clone allocates. Copy does not.
Decision matrix
Use Copy types when you are working with primitive values like integers, floats, booleans, or characters. Use Copy types when you want assignment and function calls to duplicate data without transferring ownership. Derive Copy and Clone for small structs that only contain Copy fields.
Use move when you are passing a closure to another thread and the closure needs to own its captured data. Use move when a closure must outlive the scope where it was created. Use move when you want to explicitly transfer ownership into a closure instead of borrowing by reference.
Reach for .clone() when you need a second independent copy of a String, Vec, or custom type that does not implement Copy. Reach for references (&T) when you only need to read or temporarily modify data without taking ownership.