Skip to content
intermediate Phase 2 · Ownership and Move Semantics

Partial Moves and Field Ownership

Work with struct fields that move individually while other fields stay usable.

40m
0 problems
Topic Progress 0%

Moving One Field Leaves the Rest Alive — the Compiler Tracks Fields, Not Just Values

Ownership is usually described at the level of whole values, but the borrow checker actually tracks ownership per field. Moving one field out of a struct does not kill the struct — it kills that field:

struct User {
    name: String,
    email: String,
}

fn main() {
    let user = User {
        name: String::from("ada"),
        email: String::from("ada@example.com"),
    };

    let name = user.name;      // PARTIAL MOVE: `name` field moved out
    println!("{}", user.name);   // error[E0382]: borrow of partially moved value: `user`
    println!("{}", user.email);  // FINE — the untouched field is still owned by `user`
}

The error message is precise: "partially moved value" — not "moved value". The compiler knows user.email is intact and lets you read it. This granularity exists because struct fields are separate, named slots: the checker can mark one slot moved-out while the rest remain live.

What is lost after a partial move is whole-struct use. Anything requiring all fields at once fails:

let name = user.name;      // move the name field
let email = user.email;    // ok — second field, moved independently
// Passing the entire struct anywhere is now impossible:
print_user(user);          // error[E0382]: ... `user` partially moved

Destructuring in one statement moves several fields cleanly, which reads better than three separate field moves:

let User { name, email } = user;   // both fields moved; `user` is fully consumed
println!("{} {}", name, email);

Mixing by-value and by-reference bindings in the same pattern gives you the common "take some, borrow the rest" shape without any clone:

let User { name, ref email } = user;
//  name: String (moved) · email: &String (borrowed — user.email still owned, but `user`
//  as a whole is still partially moved because `name` left)

The tuple/array-struct versions behave identically — let (x, rest) = pair; moves x and leaves rest — and the same per-element granularity applies to tuples. One asymmetry already met in Move Semantics in Practice deserves repeating here: this works for structs and tuples, but not for Vec or arrays accessed by index. v[0] cannot move an element out (E0507) because a hole in contiguous storage would corrupt the collection's length invariant. Named fields can be individually owned; indexed elements cannot.

Here is the exact diagnostic, and it is more encouraging than it looks:

error[E0382]: borrow of partially moved value: `user`
 --> src/main.rs:9:20
  |
6 |     let name = user.name;
  |                ---- value moved here
...
9 |     println!("{}", user.name);
  |                    ^^^^ value borrowed here after move
  = note: partial move occurs because `user.name` has type `String`

The note names the field, not the struct — and everything the note does not forbid still works. Borrows of the surviving fields are legal, nested field access is legal, and moving a second field out is legal:

let name = user.name;          // partial move #1
let email = user.email;        // partial move #2 — a second field, still fine
report(&user.address);         // & of an untouched field — fine
// send(user);                // only THIS fails: it needs every field

Nesting deepens the same tracking: moving user.address.city marks that leaf moved, leaves user.address.zip readable, and still blocks any whole-struct use of user. The checker is walking the field tree, not flipping a single moved bit.

The design lesson sits in the error, not just the fix. If a struct must survive having a field extracted, that field should be reinitialized (see below) or the extraction should happen through a reference — and if you find yourself wanting to move a field and keep passing the struct around, the API shape is usually wrong: either the caller should own the parts (destructure and return them) or the struct should own the operation (a method that consumes self). Partial moves are a bridge for local code, not a pattern to build interfaces on.

Patterns Move What They Bind by Value

Partial moves are a special case of a general rule: a pattern that binds a non-Copy value by value moves it. Every let pattern, function argument pattern, and match arm follows this:

struct Config {
    host: String,
    port: u16,
}

fn main() {
    let cfg = Config { host: String::from("localhost"), port: 8080 };

    match cfg {
        Config { host, port } => {
            //  host: String (MOVED out of cfg)
            //  port: u16    (Copy — duplicated, cfg.port still usable)
            println!("{}:{}", host, port);
        }
    }
}

Notice the mixed outcome: host moves because String is not Copy; port copies because u16 is. Whether a binding steals or duplicates is decided entirely by the bound type's Copy-ness — patterns do not add new rules, they apply the ones you know.

To borrow within a pattern instead of moving, add ref (or use & in the pattern):

match cfg {
    Config { ref host, port } => println!("{}:{}", host, port),  // host: &String
}
// cfg.host still owned and usable (nothing moved); cfg.port copied.

ref binds by shared borrow; ref mut binds by mutable borrow. Since Rust 2021, match ergonomics let you drop the ref keyword when matching a reference — match &cfg { Config { host, port } => ... } binds host: &String automatically, because binding a non-Copy field from a reference can only be a borrow (there is nothing to move out of a borrow). Knowing both forms lets you read older and newer code alike.

The pattern rule also explains a recurring compiler behavior: after destructuring moves fields, the original name is only usable if every field survived:

let Config { host, port } = cfg;
// host: String, port: u16 — cfg fully consumed regardless of Copy-ness of port,
// because `host` left. Using cfg afterward is E0382.

Partial-move granularity returns when you rebind selectively — let host = cfg.host; alone leaves cfg.port readable. The mental model to carry: patterns are ownership-routing declarations. By-value binding of a non-Copy type = move; by-value binding of a Copy type = duplicate; ref/& binding = borrow. Everything else you need to know about moves, you already know — patterns just choose which door each field walks through.

Patterns can also decline to touch fields at all, which is what the rest pattern does:

let Config { host, .. } = cfg;     // binds host only; port is never bound, never moved
println!("{}", cfg.port);          // legal: port was never touched by the pattern

.. skips binding, so those fields keep their ownership with the original value — combine it with a ref binding and you can destructure a large struct while moving nothing:

let User { ref name, .. } = &user;   // name: &String, nothing moved

Function arguments are patterns too, which is why this compiles and consumes its argument:

fn split(User { name, email }: User) -> (String, String) { (name, email) }

At the arm level, one more feature of match matters: arms can carry guards, and a guarded arm does not commit its bindings until the guard passes. Values moved in an arm that is never taken are irrelevant; the compiler checks each arm independently, so moving the same field in two different arms is fine — only one arm executes:

match cfg {
    Config { host, .. } if host.is_empty() => fallback(),
    Config { host, .. } => serve(host),      // separate arm, separate move
}

The @ binding operator closes out the toolbox — bind the whole value while also testing it:

match len {
    n @ 0..=9 => println!("single digit: {n}"),
    n => println!("{n} digits or more"),
}

Read n @ 0..=9 as "match this range, and call the matched value n." Every one of these forms obeys the single rule from the top of the chapter: binding by value of a non-Copy type moves, binding by reference borrows, and .. does neither.

Making a Partially Moved Struct Whole Again

After let name = user.name; the struct is partially moved: individual fields work, the whole value does not. There are exactly two escape routes, and both work by reinitializing the moved-out field.

Route one — assign a new value to the moved field. For a plain struct this restores whole-struct usability:

struct Session { user: String, token: String }

fn main() {
    let mut s = Session { user: String::from("ada"), token: String::from("t0") };
    let token = s.token;             // partial move
    s.token = String::from("t1");   // field reinitialized — s is whole again
    validate(s);                     // ok: entire struct passed
}

The binding must be mut, and the order matters — reassignment is only legal once the field is moved-out or uninitialized, not while it still owns a value you have not moved (that would need a plain overwrite, which drops the old value, fine under mut).

Route two — Option::take, the standard-library idiom for movable-out-of fields. Wrap the field in Option<T> at design time, then take it:

struct Cache {
    data: Vec<u8>,
    staged: Option<Vec<u8>>,   // designed for taking
}

let mut cache = Cache { data: vec![1], staged: Some(vec![2]) };
let staged: Vec<u8> = cache.staged.take().expect("staged exists");
// cache is WHOLE (staged is now None) — usable in full, repeatedly

Option::take swaps the field with None and returns the old value in one move — no clone, no partial-move state left behind. This is exactly how MutexGuard-style APIs and builder patterns hand out owned values from behind a shared reference. The related std::mem::replace(value, new) swaps in an arbitrary replacement, useful when you need a temporary stand-in rather than absence.

Why some types never allow partial moves: types with a Drop impl. The destructor needs the complete value to run correctly — a File's drop closes its descriptor, and dropping a half-present File is undefined responsibility. The compiler enforces it:

struct Conn { name: String, sock: Socket }   // Socket: impl Drop
let name = conn.name;   // error[E0509]: cannot move out of type `Conn`,
                        //              which implements the `Drop` trait

E0509 — "cannot move out of type which implements the Drop trait" — is the signal that your type must be consumed whole (move the entire struct) or restructured (put a take-able Option<T> or an Arc around the shared part). Partial moves are available precisely for the types where the compiler can still guarantee a correct, complete drop — which is why the feature tracks non-Drop structs and tuples but stops at Vec elements and Drop types.

There is one more error you will meet in this area, and it is the mirror image of E0509: moving a field out from behind a reference.

impl Session {
    fn take_token(&self) -> String {
        self.token        // error[E0507]: cannot move out of `self.token`
                          //              which is behind a shared reference
    }
}

&self promises the session stays intact for its other readers, so extracting an owned field from it would give the struct a hole nobody could fill. The three sanctioned answers, in the order a reviewer will suggest them: return &String (or &str) when the caller only reads; return String by cloning when the caller genuinely needs an owned copy; restructure the field as Option<String> and expose take_token(&mut self) when ownership must leave exactly once. The fourth answer — reach for unsafe — is not one.

Option<T> is where this topic and the standard library shake hands. Look at what the signature of take is really saying: pub fn take(&mut self) -> Option<T> — "give me exclusive access and I will hand you the value while leaving a valid placeholder behind." The same trick appears inside MutexGuard, inside channels (Receiver::try_recv moves messages out of shared queue slots), and inside any builder that wants to release a finished part. When you design a struct whose fields must leave, design them as Option<T> from day one, because retrofitting take-ability onto a plain field means changing the type and every place that reads it.

The complete decision table for the topic: field extraction on a plain struct — allowed, whole use blocked until reinit; on a Drop struct — forbidden (E0509), consume whole; through a reference — forbidden (E0507), take or clone; out of a collection by index — forbidden (E0507), iterate with into_iter. Four rules, one principle: whatever you extract must leave something valid behind, because something will still try to drop it.

Quiz

1. After `let name = user.name;` on a struct with fields name: String and email: String, what remains legal?

Question 1 options

2. In `let Config { host, port } = cfg;` where host: String and port: u16, what are the ownership outcomes?

Question 2 options

3. You need to hand an owned Vec out of a field, repeatedly, without cloning. What is the idiomatic design?

Question 3 options

4. Why does `let name = conn.name;` fail with E0509 when Conn implements Drop?

Question 4 options

Flashcards

Question

What is a partial move, and what can still be done with the struct afterward?

Answer

Moving one field out of a struct (e.g. let name = user.name;) — the field is owned by the new binding, other fields remain owned by the struct and stay individually usable. Whole-struct uses (passing it by value, moving it) are rejected until the moved field is reinitialized. The error names the partially moved value (E0382 variant).

Question

How do patterns decide between moving and copying each field?

Answer

By-value binding follows the bound type's Copy-ness: non-Copy (String) → move; Copy (u16, f64) → duplicate. `ref` (or matching through a reference, per match ergonomics) binds by shared borrow instead, leaving ownership with the struct. One statement can therefore move some fields, copy others, and borrow the rest.

Question

Why can't a struct that impls Drop be partially moved? (E0509)

Answer

The destructor receives the whole value (&mut self) and must run on complete, initialized state; a field moved out would leave drop operating on an incomplete struct — unsound. Compiler: 'cannot move out of type X, which implements the Drop trait'. Fix: move the whole value, or restructure with Option::take / Arc.

Question

Restore whole-struct usability after a partial move — two routes.

Answer

1) Reinitialize the moved field (needs `mut`): s.token = String::from("new"); — s is whole again. 2) Design the field as Option<T> and use field.take() — returns the owned value and leaves None behind, keeping the struct whole and reusable. mem::replace(value, stand_in) is the variant that swaps in a non-None replacement.

Question

Partial moves: structs/tuples vs Vec and arrays — what is allowed and why not?

Answer

Struct fields and tuple elements are individually named slots the checker can mark moved-out; indexed collection elements cannot move out (E0507 'cannot move out of index') because a hole would violate the collection's length/contiguity invariants. Consume via into_iter(), or borrow via &v[i].

Revision Notes

Key Takeaways

  • 1. Ownership is tracked per field: moving one field leaves the others usable but bars whole-struct use until reinitialization
  • 2. Patterns apply the Copy rule per binding — non-Copy moves, Copy duplicates, ref/& borrows — so one destructuring statement can route each field differently
  • 3. E0509 'cannot move out of type which implements Drop' is the compiler protecting the destructor's need for complete state; restructure with Option<T> instead
  • 4. Option::take is the designed-in idiom for handing out owned values from fields — swap-with-None in one move, whole struct preserved, repeatable per slot
  • 5. Struct fields can move individually; indexed collection elements never can (E0507) — named slots vs invariants of contiguous storage

Interview Tips

  • • "What is a partial move?" — moving one field out of a struct; other fields stay usable, whole-struct use is banned until reinit; contrast with E0507 (collections forbid it) to show the boundary
  • • "How do you return an owned field from behind a &self method?" — you cannot move out of a borrow; the design answer is Option<T> + take() (or mem::replace), with Arc<T> when several owners must share — knowing why the naive version fails earns the point
  • • "Why can't Drop types be partially moved?" — drop(&mut self) must run on complete initialized state; E0509 enforces it. Mention that this is why types with custom cleanup are consumed whole
  • • "match ergonomics and ref — what changed?" — matching a reference binds non-Copy fields as borrows automatically (Rust 2018+), so explicit `ref` is mostly legacy syntax; shows familiarity with both eras of code

Cheat Sheet

Partial Moves and Field Ownership

Core: the borrow checker tracks ownership PER FIELD.

let name = user.name;        // partial move: String field moved
println!("{}", user.email); // ok — untouched field
print_user(user);            // E0382 — whole-struct use forbidden

Destructuring moves what it binds by value:

let User { name, email } = user;       // both moved — user consumed
let User { name, ref email } = user;   // name moved, email: &String borrowed

Copy fields (u16, f64) copy even in by-value patterns.

Escape routes (both reinitialize):

Route Code When
Assign new value s.token = String::from("t1"); (needs mut) plain struct
Option::take cache.staged.take() → leaves None designed-for fields; repeatable
mem::replace swap in a stand-in need non-absent placeholder

Blocker:

error[E0509]: cannot move out of type `Conn`,
              which implements the `Drop` trait

Drop needs the complete value → consume whole or restructure (Option / Arc).

Not allowed anyway: moving out of v[i] (E0507) — collections keep invariants; use into_iter() or &v[i].

Continue Learning

Before You Start

Next Topics

Related Reading