When the compiler steals your variable
You write a function that accepts a String. You pass your variable to it. The very next line tries to print that same variable. The compiler refuses to compile. It feels like Rust is being stubborn, but it is actually preventing a silent memory crash. The language treats data handoffs like physical objects. Once you hand something over, you no longer hold it. If you need to keep using it, you have to make a copy or ask to borrow it instead.
Ownership is a handoff, not a photocopy
Rust enforces a single-owner rule for every piece of heap-allocated data. The owner is responsible for cleaning the data up when its scope ends. That rule keeps the language safe and fast. It also occasionally gets in your way when you want to share data across multiple function calls.
Think of a String like a signed deed to a house. The deed lives on the stack. The actual house, which contains the character bytes, lives on the heap. When you pass the String to a function, you are handing over the deed. The compiler tears up your copy to prevent two parts of your program from claiming the same house. If you later try to use your original variable, the compiler stops you because you literally do not hold the deed anymore. This rule eliminates use-after-free bugs at compile time. You cannot accidentally read memory that another part of your program already cleaned up.
The minimal fix: clone or borrow
You have two direct paths when you need to keep using a value after passing it somewhere else. You can clone the value, or you can pass a reference instead of the value itself.
/// Prints a message and takes ownership of the string.
fn takes_ownership(message: String) {
println!("Received: {message}");
}
fn main() {
let original = String::from("important data");
// Clone creates a fresh heap allocation with identical content.
// The original variable keeps its deed and remains valid.
let copy = original.clone();
takes_ownership(copy);
println!("Original is still alive: {original}");
}
Cloning duplicates the underlying data. The original String and the cloned String each own their own heap allocation. They are completely independent after the clone happens. Modifying one does not affect the other.
If you only need to read the data, cloning wastes memory and CPU cycles. Pass a reference instead.
/// Reads a message without taking ownership.
fn reads_only(message: &str) {
println!("Reading: {message}");
}
fn main() {
let text = String::from("borrowed data");
// The ampersand creates a reference. No heap allocation occurs.
// The function borrows the data for the duration of the call.
reads_only(&text);
println!("Still usable: {text}");
}
References let multiple parts of your code look at the same data without copying it. The borrow checker tracks how long each reference lives and guarantees they never outlive the original owner.
What actually happens under the hood
When you call String::from, Rust allocates a block of memory on the heap large enough to hold your characters. It writes the bytes there and stores a pointer, a length, and a capacity on the stack. That stack struct is your String variable.
When you pass that String to a function without &, Rust performs a move. The compiler copies the three stack fields into the function's parameter slot. It then marks your original variable as uninitialized. The compiler does not copy the heap data. It only moves the pointer. When the function returns, the parameter goes out of scope. The Drop implementation for String runs, freeing the heap memory. If you tried to use your original variable after that, you would be pointing at freed memory. The compiler prevents this by rejecting the code with E0382 (use of moved value).
When you call .clone(), Rust allocates a second block on the heap. It copies the bytes from the first block to the second. It creates a new stack struct pointing at the second block. Both stack structs are now valid. Each will free its own heap block when it goes out of scope. No double free can occur.
When you pass &String or &str, Rust creates a fat pointer containing the address and the length. It does not allocate. It does not copy the bytes. It simply points to the existing data. The borrow checker verifies that the reference lives shorter than the owner. When the function returns, the reference is dropped. The owner remains untouched.
Real code: processing a config file
Real programs rarely pass strings around in isolation. You usually read data, transform it, and need to keep the original for logging, error reporting, or retry logic. Here is how you structure that without fighting the compiler.
/// Validates a configuration line and returns a status code.
fn validate_config(line: String) -> i32 {
// The function owns the string, so it can modify or store it.
let trimmed = line.trim().to_lowercase();
if trimmed.starts_with("error") {
500
} else {
200
}
}
fn main() {
let raw_input = String::from("ERROR: disk full");
// We need the original string for a later audit log.
// Cloning avoids the move without changing the function signature.
let audit_copy = raw_input.clone();
let status = validate_config(raw_input);
// The original data is still available for reporting.
println!("Status: {status} | Source: {audit_copy}");
}
This pattern appears constantly in parsers, network handlers, and data pipelines. You clone when you need independent ownership. You borrow when you only need to inspect. You move when you are intentionally transferring responsibility to another function or data structure.
When the borrow checker draws a line
The compiler will reject code that tries to use a moved value. You will see E0382 (use of moved value) when you pass a String to a function and then try to read it. The error message points to the exact line where the move happened and the line where you tried to use the dead variable.
You will also see E0502 (cannot borrow as mutable because it is also borrowed as immutable) if you try to mutate a value while an active reference exists. Rust prevents data races and aliasing violations by enforcing strict borrowing rules. You cannot hold a mutable reference and an immutable reference to the same data at the same time.
If you find yourself cloning large strings repeatedly, you are likely paying a performance tax. Profile your code. If the clone is inside a tight loop, switch to references. If the function signature requires ownership but you only need to read, change the signature to accept &str. The community convention is to prefer &str in public APIs. It accepts string literals, String slices, and &String without extra work. It keeps your functions flexible and avoids hidden allocations.
Another convention worth knowing: String::clone() performs a deep copy. It allocates and copies bytes. This differs from Rc::clone(), which only bumps a reference counter. The naming looks identical, but the cost is completely different. Read the type signature before you call clone in performance-sensitive code.
Treat every move as a deliberate transfer of responsibility. If you are not sure who should own the data, borrow it.
Choosing your path forward
Use clone() when you need independent ownership and the data size is small enough that the allocation cost is acceptable. Use clone() when you must store the value in a collection that outlives the current scope. Use &str or &T when you only need to read or inspect the data and want zero allocation overhead. Use &mut T when you need to modify the data in place without taking ownership. Use direct ownership (String, Vec<T>, etc.) when the function is responsible for cleaning up the data or storing it long-term. Reach for Rc<T> or Arc<T> when multiple owners must share the same heap allocation and you want to avoid deep copies.
Trust the borrow checker. It usually has a point.