How to fix Rust E0382 value used after being moved

The E0382 error occurs because Rust's ownership system prevents using a value after it has been moved to another variable, ensuring memory safety without garbage collection.

The variable is dead. Long live the variable.

You write a function that takes a String. You call it. Then you try to print the String again. The compiler screams E0382. You didn't copy the data; you moved it. Rust thinks the variable is dead. This happens to everyone. It's the first wall you hit when learning ownership.

In Python or JavaScript, variables are labels on boxes. You can slap multiple labels on the same box. Rust is different. Variables are the boxes themselves. If you hand the box to someone else, you no longer have it. The label doesn't stay. The box moves.

Ownership is physical

Rust treats variables like physical objects. When you assign let b = a, you are handing the object from a to b. a no longer holds the object. It's gone. This prevents double-free bugs. If two variables owned the same memory, both would try to clean it up when they go out of scope. That crashes programs. Rust stops that at compile time.

Think of a signed deed to a house. You can't sign the deed to two people at once. If you sign it to Alice, you no longer own the house. Alice does. Rust enforces this rule for every value that owns resources.

Minimal example

fn main() {
    // Create a String on the heap. s1 owns the pointer, length, and capacity.
    let s1 = String::from("hello");
    
    // Move ownership from s1 to s2. s1 is now invalid.
    let s2 = s1;
    
    // Compiler rejects this: s1 was moved, so it holds no data.
    // Error E0382: use of moved value: `s1`
    println!("{}", s1);
}

The compiler rejects this with E0382 (use of moved value). It points to the move and the use. The message is clear: s1 was moved into s2. You can't use s1 anymore.

What happens in memory

Look at the memory. String stores three numbers: a pointer to the heap, the length, and the capacity. s1 holds these three numbers. The assignment let s2 = s1 copies those three numbers into s2. Now both variables point to the same heap allocation.

Rust marks s1 as invalid. You can't read s1 anymore. If you could, and s1 went out of scope, it would free the heap. Then s2 would free the same heap. That's a double-free crash. Rust kills the program at compile time to save you from that runtime crash.

When a variable goes out of scope, Rust calls drop. For String, drop frees the heap. If two variables owned the same String, drop would run twice. The second drop would access freed memory. That's undefined behavior. Rust prevents this by invalidating the first variable. The move is the mechanism. Once moved, the first variable can't call drop. Only the new owner calls drop. This guarantees single ownership at all times.

Copy types don't move

Not all types move. Primitive types like i32, bool, and f64 implement the Copy trait. When you assign let b = a for an i32, Rust copies the bits. a remains valid. The compiler knows these are small and cheap to copy. You don't get E0382 for i32.

fn main() {
    // i32 implements Copy. Assignment copies the value.
    let x = 5;
    let y = x;
    
    // x is still valid. No move happened.
    println!("x is {}", x);
}

String does not implement Copy. It owns heap memory. Copying the pointer would create two owners. Rust forbids that. If you define your own struct, you can derive Copy if all fields are Copy. Otherwise, it moves.

Primitives copy. Complex types move. The compiler enforces this distinction. Don't assume a type copies just because it's small.

Realistic patterns

E0382 often appears when passing values to functions. Functions that take ownership are powerful but restrictive.

/// Takes ownership of a String to process it.
fn process_data(data: String) {
    // data owns the String. It will be dropped when this function returns.
    println!("Processing: {}", data);
}

fn main() {
    // input owns a heap-allocated String.
    let input = String::from("user_input");
    
    // Move input into process_data. Ownership transfers to the parameter.
    process_data(input);
    
    // input is invalid here. The function took it.
    // Error E0382: use of moved value: `input`
    println!("Done with {}", input);
}

The function process_data takes String by value. It owns the data. When main calls it, input moves into the function. input is dead in main.

You can move a value into a function and get it back. The function takes ownership, does work, and returns the value. This is efficient. No cloning needed. The ownership transfers in and out.

/// Appends text and returns the owned String.
fn append_to(mut data: String) -> String {
    data.push_str(" world");
    data
}

fn main() {
    let mut s = String::from("hello");
    
    // Move s in, get it back. Ownership chain stays clean.
    s = append_to(s);
    
    println!("{}", s);
}

Moving in and out is efficient. It avoids cloning. Use this pattern when a function transforms data and returns the result. The ownership chain stays clean.

E0382 also happens with struct fields. If you move a field out of a struct, the struct becomes partially moved. You can't use the struct anymore. Rust prevents this to keep structs consistent.

struct Config {
    name: String,
    value: i32,
}

fn main() {
    let config = Config {
        name: String::from("debug"),
        value: 42,
    };
    
    // Move the name field out.
    let name = config.name;
    
    // config is partially moved. Can't use it.
    // Error E0382: use of moved value: `config.name`
    println!("Config name: {}", config.name);
}

The struct config is invalid after moving name. You can't access value either. Rust treats the struct as a single unit. If you move a part, the whole is compromised.

Pitfalls and conventions

Cloning is not free. String::clone allocates new memory. It copies every byte. If you clone a 1MB string, you allocate 1MB and copy 1MB. Doing this in a loop kills performance. Profile your code. If cloning is the bottleneck, switch to references. Or use Rc if you need shared ownership.

When you use Rc, the convention is Rc::clone(&rc). Writing rc.clone() compiles, but it looks like a deep copy. The explicit form signals to readers that you are bumping a reference count, not duplicating data. Follow the convention. It makes code readable.

use std::rc::Rc;

fn main() {
    let s1 = Rc::new(String::from("hello"));
    
    // Convention: use Rc::clone to show you're cloning the reference.
    let s2 = Rc::clone(&s1);
    
    println!("s1: {}", s1);
    println!("s2: {}", s2);
}

Rc uses a simple counter. It's fast. But it's not thread-safe. If you send an Rc to another thread, the compiler rejects it. Arc uses atomic operations. Atomics are slower. They ensure the counter updates correctly across cores. Use Arc only when threads share the data. Rc is for single-threaded graphs and caches.

Sometimes the fix is to change the design. If a function takes ownership, it implies the function is responsible for the data. If the caller needs the data later, the function shouldn't take ownership. Change the signature to take a reference. Or have the function return a new value. Or use a builder pattern. E0382 often signals a mismatch between the API and the usage. Fix the API, not just the call site.

Decision matrix

Use clone() when the data is small or you genuinely need an independent copy. Cloning a String allocates new memory and copies bytes. It costs CPU and memory. Only clone when the caller needs to keep the original and the callee needs ownership.

Use a reference (&T) when the callee only needs to read or modify the data temporarily. References are cheap. They are just pointers without ownership. The original variable stays alive. This is the most common fix for E0382.

Use Rc<T> when multiple parts of your program need to own the same data on a single thread. Rc tracks how many owners exist. The data stays alive until the last owner drops. It adds a small overhead for the counter.

Use Arc<T> when you need shared ownership across threads. Arc uses atomic operations for the counter, which is thread-safe. It's heavier than Rc. Use it only when threads are involved.

Restructure your code when you find yourself cloning large data just to satisfy the compiler. Often this means a function is taking ownership when it shouldn't. Change the function signature to take a reference. Or return the value back to the caller.

Start with references. Borrowing is the default path. Move only when you have a reason to take ownership. The compiler will guide you.

Where to go next