How ownership works with match expressions

Match expressions move ownership of the value into the matched arm, consuming the original variable unless a reference is used.

The match that steals your data

You're building a log parser. You have a LogEntry enum that holds different kinds of events. You match on an entry to extract the message text and print it. Then you try to write the entry to a backup buffer. The compiler rejects you with E0382 (use of moved value). You didn't expect a pattern match to vanish your data.

A match expression moves ownership by default. The patterns inside the match act as destructuring hands. They reach into the value and pull out the pieces. When a pattern binds a variable to a field, that variable becomes the new owner of that field. The original value is consumed. If you need the value after the match, you have to change how you match.

How patterns take ownership

Rust's ownership rule requires every value to have exactly one owner. A match expression respects this rule strictly. When you write match value, you are handing value to the match expression. The match expression then distributes the parts of value to the variables bound in the patterns.

If the value contains owned data like a String or a Vec, the pattern variables take ownership of that data. The original binding is marked as moved. The compiler prevents you from using the original binding after the match because the data no longer lives there. It lives in the variables inside the match arms.

enum Packet {
    Data(Vec<u8>),
    Control(String),
}

/// Process a packet and consume it.
fn process_packet(packet: Packet) {
    // The match takes ownership of `packet`.
    // The patterns move the inner data into `bytes` and `msg`.
    match packet {
        Packet::Data(bytes) => {
            // `bytes` owns the Vec<u8>.
            // The Vec has been moved out of the enum.
            println!("Processing {} bytes", bytes.len());
        }
        Packet::Control(msg) => {
            // `msg` owns the String.
            println!("Control message: {}", msg);
        }
    }
    // `packet` is moved here.
    // Attempting to use `packet` would trigger E0382.
}

The compiler tracks this flow precisely. It sees packet entering the match. It sees the patterns binding bytes and msg. It marks packet as uninitialized after the match block. Any code that tries to read packet afterward gets rejected. This isn't a limitation. It's the compiler enforcing memory safety. If the data moved, the old location is invalid.

The Copy exception

There is one case where a match doesn't move. Types that implement the Copy trait are duplicated instead of moved. If you match on an i32, a bool, or a tuple of copies, the value remains available after the match. The compiler inserts a copy operation automatically.

This is why matching on integers feels harmless. The data is small and trivial to duplicate. The original variable still holds a valid copy. The match arms get their own copies. Ownership isn't transferred because Copy types don't have exclusive ownership semantics. They are value types that can exist in multiple places simultaneously.

If your enum contains only Copy fields, matching on the enum moves the enum itself, but the fields are copied into the bindings. The enum is still consumed. You can't use the enum after the match. The difference is that the data inside the fields survives in the bindings without being moved out.

Matching on references to keep data alive

Real code rarely wants to consume data just to read it. You usually want to inspect the value, make a decision, and keep the value alive for later use. The solution is to match on a reference instead of the value.

Pass &value to the match. The patterns now bind references. You get read access to the fields without stealing ownership. The original value stays intact. You can use it after the match block.

/// Process a packet without consuming it.
fn process_and_log(packet: Packet) {
    // Match on a reference to borrow the packet.
    // The patterns bind references to the inner data.
    match &packet {
        Packet::Data(bytes) => {
            // `bytes` is &Vec<u8>.
            // We borrowed the Vec, we didn't move it.
            println!("Inspecting {} bytes", bytes.len());
        }
        Packet::Control(msg) => {
            // `msg` is &String.
            println!("Control: {}", msg);
        }
    }
    // `packet` is still valid.
    // We only borrowed it during the match.
    log_packet(&packet);
}

fn log_packet(packet: &Packet) {
    // Implementation omitted.
}

This pattern is the standard way to handle enums in Rust. You match on &value to read. You match on &mut value to mutate. You match on value only when you truly want to consume the data and take ownership of its parts.

Match ergonomics reduce boilerplate

Rust 2018 introduced match ergonomics. This feature changes how you write borrows in patterns. You can match on &value and write patterns as if you were matching the value directly. The compiler inserts the necessary references automatically.

Before ergonomics, matching on a reference required explicit reference patterns like &Packet::Data(bytes) or legacy ref patterns. Ergonomics let you write Packet::Data(bytes) and the compiler infers that bytes should be a reference because you matched on &packet.

fn check_packet(packet: &Packet) {
    // Match on &packet, but write patterns naturally.
    // The compiler infers that `bytes` and `msg` are references.
    match packet {
        Packet::Data(bytes) => {
            // `bytes` is &Vec<u8> thanks to ergonomics.
            // No need to write &Packet::Data(bytes).
            if bytes.len() > 1024 {
                println!("Large packet detected");
            }
        }
        Packet::Control(msg) => {
            // `msg` is &String.
            println!("Control: {}", msg);
        }
    }
}

This makes the code cleaner and easier to read. You don't clutter patterns with ampersands. The compiler handles the borrowing logic. You focus on the structure of the data.

Convention aside: The community considers ref patterns legacy. You might see Some(ref x) in older codebases. That syntax borrows x. Modern Rust prefers matching on &value and letting ergonomics handle the binding. Write match &opt { Some(x) => ... }. It does the same thing with less noise. New code should use ergonomics, not ref.

Pitfalls and compiler errors

Matching on ownership trips up beginners in predictable ways. The compiler catches these mistakes, but the errors can be confusing if you don't know what to look for.

The most common error is E0382 (use of moved value). You match on a value, consume it, and then try to use it again. The compiler tells you the value was moved. The fix is to match on a reference. Add & to the match head.

Another common error is E0507 (cannot move out of borrowed content). You match on a reference, but you try to move data out of the pattern. The compiler stops you. You can't steal data from a borrow. If you need ownership, match on the value, not the reference. If you need to keep the value, clone the data inside the arm.

fn bad_extract(packet: &Packet) {
    // Match on reference.
    match packet {
        Packet::Data(bytes) => {
            // `bytes` is &Vec<u8>.
            // This function call expects Vec<u8>, not &Vec<u8>.
            // The compiler rejects this with E0308 or E0507.
            take_ownership(bytes);
        }
        _ => {}
    }
}

fn take_ownership(data: Vec<u8>) {
    // Consumes the Vec.
}

The error here is a type mismatch. You have a reference, but the function wants ownership. You can fix this by cloning the data. take_ownership(bytes.clone()) works. The clone creates a new owned Vec. The original reference stays valid. Cloning costs memory and time. Use it only when you actually need a separate copy.

Partial moves are another trap. You can match on a struct and move only some fields. The struct becomes partially moved. You can't use the struct as a whole anymore, but you can use the fields that weren't moved. This is advanced and rarely needed. It makes the code harder to reason about. Avoid partial moves unless you have a specific performance requirement. Stick to matching on references for safety and simplicity.

When to use each match form

Choosing the right match form depends on what you want to do with the data. Use the form that matches your intent. The compiler enforces the distinction.

Use match value when you want to consume the value and take ownership of its parts. This is appropriate for parsers that extract data and discard the wrapper, or for state machines that transition to a new state and drop the old one. The value is gone after the match.

Use match &value when you need to read the value and use it again after the match. This is the default choice for most code. You inspect the data, make decisions, and keep the data alive. Ergonomics make this form clean and readable.

Use match &mut value when you need to mutate fields inside the match arms. This gives you exclusive access to the data. You can modify fields in place. The value remains owned by the original binding, but you can change its contents.

Reach for if let when you only care about one variant and want less boilerplate. if let follows the same ownership rules as match. if let Some(x) = opt moves x. if let Some(x) = &opt borrows x. Use if let for simple checks. Use match when you need exhaustiveness or multiple branches.

Match on the reference by default. Only move when you truly want to consume. Trust the borrow checker. It usually has a point.

Where to go next