Rust E0382 borrow of moved value — Ownership Fixes That Compile
Diagnose Rust move and borrow errors by reading the compiler's move trace, then choose borrowing, cloning, split borrows, or mem::take deliberately.
Symptoms
- error[E0382]: borrow of moved value: `config`
- error[E0502]: cannot borrow `items` as mutable because it is also borrowed as immutable
- The error points at a later use after a function call or loop iteration
- Adding clone() makes it compile but performance or semantics change
Possible causes
- A value is passed by value into a function or closure and used again afterward
- A method taking self consumes the receiver, while the caller expects to keep it
- An immutable borrow is still alive when a mutable borrow starts
- A loop moves a captured variable on the first iteration
Diagnosis steps
Read the full compiler diagnostic
cargo build 2>&1 | sed -n '/E0382/,+25p'Interpretation
Rust prints where the value was moved, where it was used later, and often a suggested fix. The move location is the real decision point, not the later use.
Ask the compiler to explain the rule
rustc --explain E0382Interpretation
The explanation shows minimal examples of moves and borrows. Compare your function signature with the example to see whether taking &T would preserve ownership.
Check for hidden clones and lints
cargo clippy --all-targets -- -D warningsInterpretation
Clippy flags redundant clones and needless borrows introduced to silence the borrow checker, which often hide a design problem.
Inspect the function signatures on the path
cargo doc --no-deps --openInterpretation
Methods that take self consume the value; methods that take &self or &mut self borrow it. The signature tells you whether the move is avoidable without changing the API.
Root cause
Rust enforces single ownership: passing a non-Copy value by value moves it, and the previous binding can no longer be used. Borrowing rules similarly prevent a mutable reference from coexisting with any other reference to the same data. The error is a compile-time proof that the code could read freed or aliased data, not a limitation to work around with clones.
Fix
Choose the cheapest correct ownership: pass &T when the callee only reads, &mut T when it mutates in place, and clone only when both sides genuinely need independent ownership. Split a struct borrow into separate field borrows, use std::mem::take for a default value, or restructure so the borrow ends before the mutation. For shared mutation, use RefCell, Mutex, or an atomic instead of fighting the checker.
Risk note
Cloning large structures on every call can dominate runtime and hide ownership bugs. RefCell moves borrow checking to runtime and can panic. Arc<Mutex<T>> adds contention and deadlock risk; it is not a default fix for E0502.
Verify the fix
Run cargo build and cargo clippy with warnings denied, then the test suite. If you added a clone, measure or document why it is necessary; if you changed an API to borrow, confirm callers still compile without extra clones.
The compiler is showing you a use-after-move
error[E0382]: borrow of moved value: `config`
--> src/main.rs:18:20
|
12 | let client = Client::new(config);
| ------ value moved here
...
18 | println!("{}", config.timeout);
| ^^^^^^^^^^^^^^ value borrowed here after move
Client::new(config) took ownership. The later println! is invalid because config no longer exists in that scope.
Decide who owns the value
cargo build 2>&1 | sed -n '/E0382/,+25p'
rustc --explain E0382
The diagnostic’s move location is the decision point. If Client only needs to read the configuration, change the parameter to &Config and pass &config. If it must store it, clone once at the boundary or let the caller give up ownership and stop using the original.
struct Client {
timeout: Duration,
}
impl Client {
fn new(config: &Config) -> Self {
Self { timeout: config.timeout }
}
}
let client = Client::new(&config);
println!("{}", config.timeout); // still valid
Handle the harder borrows
For E0502, shorten the immutable borrow before mutating, or split fields:
struct State {
users: HashMap<u64, User>,
events: Vec<Event>,
}
let State { users, events } = &mut state;
users.insert(1, user);
events.push(event);
std::mem::take(&mut value) is useful when you need the old value and can leave a default behind. A clone is legitimate when both sides need independent data, but it should be a choice, not a reflex.
Verify
cargo build and cargo clippy --all-targets -- -D warnings should be clean. Run the tests, and if you introduced a clone, confirm it is on a cold path or document why shared ownership is required. Borrowing changes are usually local and should not force clones in callers.