1. Symptoms
The Rust compiler emits error E0601 when your code attempts to use a variable whose ownership has been transferred elsewhere. The compiler displays a diagnostic message identifying the exact location where the moved value is being accessed, along with an annotation showing where the move occurred.
Typical compiler output:
error[E0601]: `variable` doesn't implement the `Copy` trait
--> src/main.rs:10:5
|
7 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
8 | let s2 = s1;
| -- value moved here
9 |
10 | println!("{}", s1);
| ^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
10 | println!("{}", s1.clone());
Common manifestation patterns:
- Attempting to access a
String,Vec<T>,Box<T>, or any custom type after assignment to another variable - Calling a function that takes ownership of an argument and then trying to use that argument afterward
- Returning a value from a function that consumes the variable
- Iterating over a collection and attempting to use the collection after the loop
The error message always includes a visual indicator pointing to where the value was originally declared and where the move took place, enabling you to trace the ownership transfer through your codebase.
2. Root Cause
Rust’s ownership system enforces a strict rule: every value has exactly one owner at any given time. When you assign a value to another variable, pass it to a function, or return it from an expression, ownership transfers to the new location. The original binding becomes invalid, and the compiler prevents any further access to prevent use-after-free bugs and dangling references.
The E0601 error occurs specifically when the compiler detects that a value is being used after ownership has been transferred. This is fundamentally different from garbage-collected languages where references can point to the same object across multiple assignments. In Rust, assignment is a transfer operation, not a reference creation.
The error only triggers for types that do not implement the Copy trait. Primitive types like i32, f64, bool, and char implement Copy, meaning their values are duplicated on assignment. heap-allocated types like String, Vec<T>, HashMap<K, V>, and custom structs without explicit Copy implementations do not implement Copy. When you assign these types, the compiler performs a move operation instead of a copy, invalidating the source variable.
Custom types can opt into Copy semantics by implementing the trait, but this requires that all contained types also implement Copy. Implementing Copy for a type that contains a String or Vec is impossible because those types are not Copy.
The borrow checker performs this analysis at compile time, ensuring memory safety without runtime overhead. When E0601 appears, the compiler has definitively determined that the use site occurs after the move site in the ownership flow.
3. Step-by-Step Fix
Fix 1: Clone the value when you need independent ownership
When you need multiple references to the same data, explicitly clone to create a deep copy. This transfers ownership of the clone instead of moving the original.
Before:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // Error: s1 was moved
}
After:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}", s1); // Works: s1 is still valid
}
Fix 2: Borrow instead of moving
Pass a reference when you only need temporary access and the original owner must retain the value.
Before:
fn take_ownership(s: String) {
println!("{}", s);
}
fn main() {
let s = String::from("hello");
take_ownership(s);
println!("{}", s); // Error: s was moved into the function
}
After:
fn borrow_string(s: &String) {
println!("{}", s);
}
fn main() {
let s = String::from("hello");
borrow_string(&s);
println!("{}", s); // Works: s is still owned by main
}
Fix 3: Reassign ownership to maintain a valid binding
When a value is consumed by a function, ensure the function returns ownership back if you need to continue using it.
Before:
fn consume_and_return(s: String) -> String {
let modified = s + " world";
modified
}
fn main() {
let s = String::from("hello");
let result = consume_and_return(s);
println!("{}", s); // Error: s was moved
}
After:
fn consume_and_return(s: String) -> String {
let modified = s + " world";
modified
}
fn main() {
let s = String::from("hello");
let result = consume_and_return(s);
println!("{}", result); // Use the returned ownership
}
Fix 4: Use the value before it moves
Access the value in an expression where ownership transfers but the final result is bound to a variable you can use.
Before:
fn main() {
let s1 = String::from("hello");
let s2 = String::from("world");
let combined = s1 + &s2;
println!("{}", s1); // Error: s1 was moved
}
After:
fn main() {
let s1 = String::from("hello");
let s2 = String::from("world");
let combined = s1.clone() + &s2; // Clone before move
println!("{}", s1); // Works with clone
}
Fix 5: Restructure code to avoid moving
Sometimes the solution involves rethinking ownership at a deeper architectural level.
Before:
fn process(data: Vec<i32>) -> i32 {
data.iter().sum()
}
fn main() {
let numbers = vec![1, 2, 3];
let total = process(numbers);
for n in numbers { // Error: numbers was moved
print!("{}", n);
}
}
After:
fn process(data: &[i32]) -> i32 {
data.iter().sum()
}
fn main() {
let numbers = vec![1, 2, 3];
let total = process(&numbers);
for n in &numbers { // Borrow instead of move
print!("{}", n);
}
}
4. Verification
After applying a fix for E0601, verify that the compilation succeeds and the program behaves as intended.
Compile the project:
cargo build
# or
rustc src/main.rs
A successful build produces no error output and generates the binary executable. The error E0601 should no longer appear in the diagnostic output.
Run the program to confirm runtime behavior:
cargo run
Verify that the output matches your expectations. Cloning creates independent data, so modifications to one binding should not affect the other:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
s2.push_str(", world");
println!("s1: {}", s1); // Prints "hello"
println!("s2: {}", s2); // Prints "hello, world"
}
Run tests to ensure the fix doesn’t break existing functionality:
cargo test
When borrowing is the chosen strategy, confirm that lifetime boundaries are respected and no dangling references exist. The compiler enforces these guarantees at compile time, so a successful build confirms correctness.
5. Common Pitfalls
Pitfall 1: Confusing &T (reference) with T (owned value)
Beginners often write &s when they intend to pass ownership, or vice versa. When a function signature expects String, passing &String causes a type mismatch error. When you want to retain ownership after a function call, the function must take a reference.
Pitfall 2: Unnecessary cloning
Cloning has a runtime cost proportional to the data size. For frequently accessed data in performance-critical code paths, cloning can introduce measurable overhead. Consider borrowing (&T) or using Rc<T> for shared ownership when clone is not strictly necessary.
Pitfall 3: Forgetting that iterators consume collections
Iterating with for item in collection moves the collection. After the loop, the collection is no longer accessible. Use for item in &collection to iterate without consuming, or use into_iter() explicitly when consumption is intentional.
Pitfall 4: Implementing Copy incorrectly
Forgetting that Copy and Clone are separate traits leads to unexpected moves. If a type contains any non-Copy field, the entire type cannot be Copy. Manually implementing Copy for a type containing a String is a compile-time error, but understanding this constraint helps avoid logic errors.
Pitfall 5: Ignoring the “does not implement the Copy trait” note
The compiler hint often includes the suggestion to “consider cloning.” This is sometimes the right solution, but often borrowing is more appropriate. Evaluate both approaches before choosing.
Pitfall 6: Assuming structs are copied by default
Custom structs do not implement Copy unless you explicitly derive or implement it. A struct containing a single String field requires String to be Copy before the struct can be Copy. Without explicit implementation, assignment causes a move.
6. Related Errors
E0382: Use of moved value (partial move)
This error occurs when you try to use a struct or enum after some of its fields have been moved, but the entire value is still in scope. Partial moves happen when destructuring assigns some fields to variables while others remain in the original binding.
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
let moved_x = p.x; // x is moved out
println!("{}", p.y); // Error: p.y is still accessible, but p cannot be used
}
E0507: Cannot move out of a static variable
This error occurs when attempting to move a value out of a context where moves are prohibited, such as static variables. Statics have a 'static lifetime and require values that live for the entire program execution, but moves create temporary bindings that violate this constraint.
E0508: Cannot move out of a borrowed content
This error occurs when attempting to move a value out of a borrowed reference without first dereferencing. You cannot move out of a reference because the reference does not own the data—moving would invalidate the original owner.
fn main() {
let s = String::from("hello");
let r = &s;
let owned = *r; // Error: cannot move out of borrowed content
}
Understanding E0601 provides the foundation for reasoning about Rust’s ownership model. The patterns that trigger this error—transferred ownership, borrowed access, and move semantics—appear throughout Rust codebases. Mastering these concepts eliminates a significant category of compilation errors and enables writing idiomatic, memory-safe Rust code.