Fixing "use of partially moved value": Moving vs. Borrowing in Patterns¶
This page takes one real compiler error and works through it end to end. It assumes you know the basic difference between moving and borrowing. If not, read Understanding Ownership, Moves, and Borrowing first; the one-line version is:
Move: ownership transfers and the old binding is dead. Borrow: temporary access; the owner keeps it.
The scenario¶
A tool checks several download sources and keeps whichever one offers the highest version. It tracks the best candidate so far in an Option<(Ver, String)>: a version and the URL it came from.
#[derive(Debug, PartialEq, PartialOrd)]
struct Ver {
major: u32,
minor: u32,
patch: u32,
}
fn pick(best: Option<(Ver, String)>, ver: Ver, url: String) -> Option<(Ver, String)> {
match best {
Some((bv, _)) if bv >= ver => best,
_ => Some((ver, url)),
}
}
In words: if we already have a best candidate and its version is at least as high as the new one, keep the existing best; otherwise, the new one wins.
Aside:
#[derive(PartialOrd)]on a struct compares fields in declaration order, like comparing tuples. Withmajor,minor,patchdeclared in that order and stored as integers,1.10.0 > 1.3.0comes out correctly. If the fields were declared in a different order, or stored as strings, the comparison would silently be wrong.
The error¶
error[E0382]: use of partially moved value: `best`
--> src/main.rs:10:39
|
10 | Some((bv, _)) if bv >= ver => best,
| -- ^^^^ value used here after partial move
| |
| value partially moved here
|
= note: partial move occurs because value has type `Ver`, which does not implement the `Copy` trait
help: borrow this binding in the pattern to avoid moving the value
|
10 | Some((ref bv, _)) if bv >= ver => best,
| +++
Reading the pattern¶
Some((bv, _)) does three things at once:
| Piece | Meaning |
|---|---|
Some(…) |
Match only if the Option holds a value. |
(bv, _) |
That value is a 2-tuple. |
bv |
Bind the first field to a new variable named bv. |
_ |
Ignore the second field. _ never binds, so it never moves anything. |
The key detail is how bv is bound. Because we're matching on best itself (an owned value, not a reference), a plain identifier in the pattern binds by value. For a non-Copy type like Ver, binding by value means moving the Ver out of the tuple, out of the Option, out of best.
Why the error happens¶
- The arm is selected, so
bvtakes ownership of theVerinsidebest. bestis now partially moved: itsStringis still there, but itsVerhas been taken.- The arm body then tries to return
bestas a whole. - A value with a hole in it can't be used as a complete value. Rust rejects it.
In book-lending terms: you took a chapter out of the book, then tried to hand the complete book to someone else.
Subtlety: guards don't move. Inside the
if bv >= verguard, Rust gives youbvby reference, so the comparison itself is fine. The move happens only once the arm is chosen and its body runs. That's why the error points atbestin the body, not at the comparison.
The compiler's suggestion, and the follow-up error¶
The suggested fix is the ref keyword:
Some((ref bv, _)) if bv >= ver => best,
ref bv means "bind bv as a reference to the field instead of moving it." That fixes the move, but compiling it produces a new error:
error[E0308]: mismatched types
--> src/main.rs:10:36
|
10 | Some((ref bv, _)) if bv >= ver => best,
| ^^^ expected `&Ver`, found `Ver`
|
help: consider dereferencing the borrow
|
10 | Some((ref bv, _)) if *bv >= ver => best,
| +
bv is now a &Ver and ver is a Ver, and PartialOrd isn't implemented between a reference and a value. Dereference it (*bv) and it compiles. This two-step dance is normal: a compiler suggestion fixes the error it's reporting, and the next one appears.
Five ways to fix it¶
All of these compile and behave identically. Pick the one that reads best in context.
1. ref in the pattern¶
fn pick(best: Option<(Ver, String)>, ver: Ver, url: String) -> Option<(Ver, String)> {
match best {
Some((ref bv, _)) if *bv >= ver => best,
_ => Some((ver, url)),
}
}
The most direct translation of the compiler's advice. ref is perfectly valid but has become less common in modern code since option 2 arrived.
2. Match on a reference (match &best)¶
fn pick(best: Option<(Ver, String)>, ver: Ver, url: String) -> Option<(Ver, String)> {
match &best {
Some((bv, _)) if *bv >= ver => best,
_ => Some((ver, url)),
}
}
When you match a reference against a pattern that doesn't start with &, Rust switches the default binding mode to "by reference" for everything inside. This is called match ergonomics (or default binding modes). Every binding (bv here) automatically becomes a reference, so nothing is moved. This is the most common modern idiom.
It might look odd that the arm can return best while &best is being matched. It works because the shared borrow ends after its last use (the guard), so by the time the arm body runs, nothing is borrowing best anymore. See Borrows end at their last use.
3. Don't match at all: is_some_and¶
The function is really asking a yes/no question about best, then returning one of two values. Say that directly:
fn pick(best: Option<(Ver, String)>, ver: Ver, url: String) -> Option<(Ver, String)> {
if best.as_ref().is_some_and(|(bv, _)| *bv >= ver) {
best
} else {
Some((ver, url))
}
}
Option::as_ref turns &Option<(Ver, String)> into Option<&(Ver, String)>, a borrowed view you can inspect without consuming the original. This is often the clearest option, and the pattern opt.as_ref().map(...) / .is_some_and(...) is worth memorising.
4. Move everything, then rebuild¶
If you're going to consume best anyway, move all its parts and reassemble them:
fn pick(best: Option<(Ver, String)>, ver: Ver, url: String) -> Option<(Ver, String)> {
match best {
Some((bv, u)) if bv >= ver => Some((bv, u)),
_ => Some((ver, url)),
}
}
No partial move, because nothing tries to use best afterwards: you only use the pieces you took. Handy when the arm wants to modify a field on the way out.
5. Make Ver Copy, if that's semantically right¶
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Ver {
major: u32,
minor: u32,
patch: u32,
}
Three u32s are 12 bytes of plain data, so Copy is entirely reasonable here, and with it the original code compiles unchanged: binding bv copies the version instead of moving it.
Only do this when it makes sense for the type itself. If Ver later gains a pre_release: String field, it can no longer be Copy, and the error comes back. Fixing the pattern (options 1–4) is robust to that change; deriving Copy isn't.
What about .clone()?¶
match best.clone() { /* ... */ } // compiles, but allocates a new String on every call
It works, but it duplicates the URL string just to look at a version number. Use clone() when you actually need a second owned value, not to sidestep a pattern that should have borrowed.
Testing it¶
Whichever fix you choose, a quick test locks in the behaviour:
#[cfg(test)]
mod tests {
use super::*;
fn v(major: u32, minor: u32, patch: u32) -> Ver {
Ver { major, minor, patch }
}
#[test]
fn keeps_highest_version() {
let mut best = None;
for (ver, url) in [(v(1, 2, 0), "a"), (v(1, 10, 0), "b"), (v(1, 3, 0), "c")] {
best = pick(best, ver, url.to_string());
}
assert_eq!(best, Some((v(1, 10, 0), "b".to_string())));
}
}
A debugging routine for move errors¶
When Rust reports a move or partial move:
- Find the owner. Here,
best. - Find the move. Look at assignments, function arguments, method calls that take
self, closures withmove,forloops over owned collections, and identifiers in patterns. - Ask whether the code needs ownership. If it only inspects, borrow:
&x,match &x,ref,.as_ref(),.iter(). - Ask whether the value is used again. If yes, don't move part of it out; if no, move all of it deliberately.
- Only then consider
CopyorClone, and decide based on what the type is, not on what makes the error go away.
Remember this¶
- An identifier in a pattern binds by value when you match an owned value, which moves non-
Copyfields. _never binds and never moves.ref namebinds by reference.match &valuemakes every binding a reference.Option::as_ref()lets you inspect anOption's contents without consuming it.- A partially moved value can't be used as a whole again.
- Reach for
Copywhen the type is genuinely plain data, and forclone()when you genuinely need two owners.
Related¶
- Understanding Ownership, Moves, and Borrowing: the underlying model.
- Architecting Rust Projects: A Comprehensive Guide: ownership in larger designs.
- Rust Reference: Binding modes: the precise rules behind match ergonomics.
rustc --explain E0382: the compiler's own explanation of this error.