How to Destructure Values in Rust

Use match patterns with curly braces or parentheses to extract and name specific values from Rust enums and structs.

Unpacking values with patterns

You just received a complex data structure from an API or a database query. It's a struct with twenty fields. You only care about three of them. Writing let host = data.host; let port = data.port; let timeout = data.timeout; feels like busywork. You want to grab what you need and drop the rest. Rust gives you a tool for this called destructuring. It lets you tear a value apart and bind the pieces to variables in a single line.

Destructuring is pattern matching applied to assignment. A pattern describes the shape of a value. When the value matches the shape, Rust extracts the inner parts and binds them to names you choose. It's not just for match arms. You can destructure in let bindings, function parameters, and for loops. The compiler checks the shape at compile time. If the shape doesn't match, the code doesn't compile. This prevents runtime errors where you assume a field exists but it doesn't.

Patterns are contracts. If the value doesn't fit, the code doesn't run.

The basics: structs and tuples

Structs use curly braces. Tuples use parentheses. The syntax mirrors how you construct the values.

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 10, y: 20 };

    // Destructure the struct. Bind x and y to new variables.
    // The names in the pattern must match the field names.
    let Point { x, y } = p;

    println!("x is {x}, y is {y}");

    // Tuples use parentheses. Order matters, not names.
    let pair = (1, "hello");
    let (first, second) = pair;

    println!("first: {first}, second: {second}");
}

When you write let Point { x, y } = p;, Rust looks at p. It sees p is a Point. It checks the pattern Point { x, y }. The pattern asks for fields named x and y. Rust finds them. It moves the value from p.x into the new variable x, and from p.y into y. After this line, p is no longer usable. Its contents have been moved out. If you try to use p again, the compiler rejects you with E0382 (use of moved value). The destructuring consumes the original value unless the type implements Copy.

Convention aside: Prefer let Point { x, y } = p; over separate assignments. The destructuring form is idiomatic. It declares your intent upfront and keeps related bindings together.

Real-world usage: enums and ignoring fields

Real code often deals with enums that have many variants and many fields. You rarely need everything. Rust provides tools to ignore what you don't want and rename what you do want.

enum Event {
    Click { x: f64, y: f64, timestamp: u64 },
    KeyPress { key: String, modifiers: Vec<String> },
}

fn handle_event(event: Event) {
    match event {
        // We only care about coordinates. Ignore timestamp explicitly.
        Event::Click { x, y, timestamp: _ } => {
            println!("Click at ({x}, {y})");
        }
        // Rename the binding. 'key' field becomes 'char_code'.
        // '..' ignores all remaining fields.
        Event::KeyPress { key: char_code, .. } => {
            println!("Pressed: {char_code}");
        }
    }
}

The .. syntax ignores all remaining fields. This is powerful for maintainability. If someone adds a device_id field to KeyPress, code using .. still compiles. Code that lists every field breaks. Use .. liberally. It makes your code resilient to struct changes.

The key: char_code syntax renames the binding. The field is key, but the variable inside the arm is char_code. This is useful when the field name clashes with a local variable or when you want a clearer name for the logic inside the arm.

Don't fight the borrow checker by moving values you need to keep. Use ref or & in the pattern.

Borrowing vs moving in patterns

Destructuring moves values by default. This causes trouble when you only have a reference. If you try to move out of a reference, the compiler rejects you with E0507 (cannot move out of borrowed content).

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

fn read_host(config: &Config) -> &str {
    // This fails: E0507. You cannot move String out of &Config.
    // let Config { host, .. } = config;

    // Solution 1: Borrow the field with 'ref'.
    let Config { host: ref host, .. } = config;
    host.as_str()

    // Solution 2: Match the reference directly.
    // let &Config { host, .. } = config;
    // host.as_str()
}

The ref keyword tells Rust to create a reference to the field instead of moving it. The variable host becomes &String. Solution 2 matches the reference directly. The & in the pattern says "the value is a reference to a Config". Rust peels off the reference and binds host as &String. Both approaches work. Solution 2 is often cleaner when you are matching a reference.

Convention aside: When destructuring a reference, let &Struct { field } = &value; is common. It reads naturally: "match a reference to a Struct, and bind field."

Nested patterns and the @ operator

Patterns can nest. You can destructure inside destructuring. This is essential for working with Option, Result, and collections.

struct Point {
    x: i32,
    y: i32,
}

fn process_opt_point(opt: Option<Point>) {
    match opt {
        // Destructure the Option, then the Point inside.
        Some(Point { x, y: 0 }) => {
            println!("Point on x-axis: {x}");
        }
        // Ignore the rest of the Point.
        Some(Point { x, .. }) => {
            println!("Other point: x={x}");
        }
        None => {
            println!("No point");
        }
    }
}

You can also use the @ operator to bind a sub-pattern to a variable while capturing the whole value. This is useful when you need to inspect the whole value later, perhaps for logging or error reporting.

enum Message {
    Error { code: u32, details: String },
    Info(String),
}

fn log_and_handle(msg: Message) {
    match msg {
        // Bind the whole Error variant to 'full_msg'.
        // Also bind 'code' to 'code'.
        Message::Error { code, .. } @ full_msg => {
            println!("Handling error code {code}");
            // full_msg is available here if needed.
            log_full_message(&full_msg);
        }
        _ => {}
    }
}

fn log_full_message(msg: &Message) {
    // Implementation omitted.
}

The @ goes between the pattern and the name. Message::Error { code, .. } @ full_msg matches the pattern, binds code, and also binds the entire matched value to full_msg.

Trust the exhaustive check. It catches bugs before they reach production.

Pitfalls and compiler errors

Destructuring is safe, but the compiler will stop you if you make mistakes.

Non-exhaustive patterns. If you miss a variant in a match, the compiler rejects you with E0005 (non-exhaustive patterns). You must handle every variant or use _ to catch the rest. This is a feature. It forces you to think about all cases.

enum Color { Red, Green, Blue }

let c = Color::Red;
match c {
    Color::Red => println!("Red"),
    // Missing Green and Blue.
    // Compiler error: E0005 non-exhaustive patterns.
}

Mismatched types. If the pattern expects a different type than the value, you get E0308 (mismatched types). This happens often with Option and Result.

let opt = Some(5);
match opt {
    // Wrong: pattern expects i32, value is Option<i32>.
    // E0308 mismatched types.
    5 => println!("Five"),
    _ => {}
}

Shadowing confusion. Destructuring can shadow variables. This is intentional.

let x = 10;
let Point { x } = Point { x: 20, y: 30 };
// x is now 20. The old x is shadowed.

Shadowing is common in Rust. It's not a bug; it's a feature for scope management. You can reuse names in inner scopes without inventing new identifiers.

Slices and or patterns

Slice patterns let you match on arrays and slices. You can match exact lengths or use .. for variable lengths.

fn analyze_slice(slice: &[i32]) {
    match slice {
        // Exact match: three elements.
        [a, b, c] => {
            println!("Three elements: {a}, {b}, {c}");
        }
        // First element, then the rest.
        [first, rest @ ..] => {
            println!("First: {first}, rest has {} items", rest.len());
        }
        // Empty slice.
        [] => {
            println!("Empty");
        }
    }
}

Or patterns let you match multiple alternatives in one arm. This reduces duplication.

match value {
    // Match 0 or 1.
    0 | 1 => println!("Small number"),
    // Match Some(x) where x is positive, or None.
    Some(x) if x > 0 | None => println!("Positive or None"),
    _ => println!("Other"),
}

The | operator combines patterns. The arm runs if any pattern matches. You can combine or patterns with guards using if.

When to use destructuring

Use destructuring in a let binding when you want to unpack a struct or tuple into local variables at the point of use. Use destructuring in function parameters when the function only needs specific fields; this avoids passing the whole struct and keeps the signature focused. Use the .. syntax when a struct has many fields and you only care about a subset; this prevents compile errors if the struct gains new fields later. Use the @ operator when you need to bind a sub-pattern to a variable while also capturing the entire value for later inspection. Use _ to ignore a specific field when you want to signal to readers that the field is intentionally discarded. Use ref or ref mut in a pattern when you need to borrow a field instead of moving it out of the original value. Use slice patterns when you need to match on the structure of a slice or array. Use or patterns when multiple variants or values share the same logic.

Destructuring is your scalpel. Use it to extract exactly what you need and nothing more.

Where to go next