Introduction
error[E0382]: borrow of moved value: 'x' -- you'll see this error a hundred times while learning Rust. Here's what it means and how to stop fighting the compiler.
Most Rust tutorials start with the theory: ownership rules, borrowing rules, lifetime annotations. Then they show you the compiler errors those rules produce. That's backwards. You encounter ownership through errors. You're writing code, it doesn't compile, and the error message mentions "moved value" or "borrowed as mutable while also borrowed as immutable." So that's where we'll start. The errors. Then why they exist. Then the fixes.
What Ownership Means in Rust
Every value has exactly one owner. When that owner goes out of scope, the value is dropped and its memory freed. That's the whole model. No GC pause, no reference counting. The compiler knows the exact moment every allocation lives and dies.
Stack vs. Heap
Fixed-size types -- integers, floats, booleans -- go on the stack. Stack allocation is a pointer bump. Cheap. Types that can grow at runtime (String, Vec<T>, HashMap) put their data on the heap, which means the allocator has to find a free block.
Stack data gets copied. Heap data gets moved. That distinction is where every ownership error comes from.
Move Semantics
This is the error you'll hit first. Assign a heap-allocated value to another variable and Rust invalidates the original. Here's what that looks like and why the compiler rejects it:
fnmain() {
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED into s2// println!("{}", s1); // ERROR: value borrowed after moveprintln!("{}", s2); // This works fine
}In Python or JavaScript, both variables would point to the same object. In C++, the assignment copies. Rust does neither. s1 is moved into s2 and immediately invalidated. One owner at a time. That single rule kills double-free bugs without a garbage collector, but it also means half your Python intuitions about variable assignment are wrong in Rust.
The Copy Trait
Not every assignment is a move:
fnmain() {
let x = 42;
let y = x; // x is COPIED, not movedprintln!("x = {}, y = {}", x, y); // Both are valid!let a = true;
let b = a; // booleans implement Copy toolet point = (3.0, 4.0);
let other = point; // tuples of Copy types are also Copyprintln!("a={}, point={:?}", a, point); // All still valid
}If it lives entirely on the stack and owns no heap resources, it can be Copy. Integers, floats, bool, char, tuples of Copy types. String, Vec<T>, anything managing heap memory -- those move.
"Can I still use the original after this assignment?" Ask whether the type is Copy. That's the first question.
References and Borrowing
If every function call moved the value in, you'd spend half your code shuffling ownership around. Borrowing lets you lend a value out instead.
Immutable References
fncalculate_length(s: &String) -> usize {
s.len() // We can read s, but we don't own it
}
fnmain() {
let greeting = String::from("Hello, Rustacean!");
// Pass a reference -- greeting is borrowed, not movedlet len = calculate_length(&greeting);
// greeting is still valid here because we only borrowed itprintln!("'{}' has {} characters", greeting, len);
// Multiple immutable references at once? No problem.let r1 = &greeting;
let r2 = &greeting;
println!("{} and {}", r1, r2);
}The & creates a reference. The function borrows, doesn't take ownership. When it returns, the borrow ends.
Mutable References
Need to modify borrowed data? &mut. But only one mutable reference at a time, and you can't mix mutable with immutable references to the same value:
fnadd_exclamation(s: &mutString) {
s.push_str("!");
}
fnmain() {
let mut message = String::from("Hello");
add_exclamation(&mut message);
println!("{}", message); // prints "Hello!"// Only ONE mutable reference at a time:let r1 = &mut message;
// let r2 = &mut message; // ERROR: cannot borrow twice
r1.push_str("!");
println!("{}", r1);
}The Borrowing Rules
Two rules.
- Either one mutable reference or any number of immutable references. Not both.
- References must always be valid. No dangling pointers.
This also kills data races. A data race needs concurrent access where at least one thread writes without synchronization. Rule 1 makes that structurally impossible at zero runtime cost. In concurrent C++ codebases, data races hide for months and surface under production load. In safe Rust, that entire category of bugs doesn't exist.
Slices: Borrowing Parts of Data
A slice borrows a contiguous window into a collection. No ownership transfer, no copying.
fnfirst_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[..i]; // Return a slice up to the space
}
}
&s[..] // No space found -- return the whole string
}
fnmain() {
let sentence = String::from("Rust is awesome");
let word = first_word(&sentence);
println!("First word: {}", word); // prints "Rust"// Array slices work the same waylet numbers = [1, 2, 3, 4, 5];
let middle = &numbers[1..4]; // [2, 3, 4]println!("Middle: {:?}", middle);
}&str is everywhere. A reference into a String or a string literal. Notice first_word takes &str rather than &String -- deref coercion lets it accept both, and that's the idiomatic way to write string-accepting functions.
Try to mutate the underlying String while a slice exists. The compiler stops you. Same rules as borrowing -- because slices are borrows.
Lifetimes Demystified
This is the annotation syntax. The compiler usually infers lifetimes. When it can't, you add these.
The concept: a lifetime tracks how long a reference stays valid. The scary part is the syntax, not the idea. A function takes two string slices and returns the longer one -- which input does the return value borrow from? The compiler can't tell without help:
Lifetime Annotations
// 'a is a lifetime parameter: both inputs and// the output share the same lifetimefnlongest<'a>(x: &'astr, y: &'astr) -> &'astr {
if x.len() > y.len() {
x
} else {
y
}
}
fnmain() {
let string1 = String::from("long string");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
println!("Longest: {}", result); // Works here
}
// println!("{}", result); // Would ERROR if uncommented:// string2 doesn't live long enough
}'a doesn't change how long anything lives. It tells the compiler about the relationship: the return value is valid for at least as long as the shorter-lived input. The compiler enforces that at every call site. You're not controlling lifetimes -- you're describing them.
Lifetime Elision Rules
"But I've written functions with references and never added lifetime annotations." Three elision rules cover most cases:
- Each reference parameter gets its own lifetime.
- One input lifetime? It applies to all output references.
- Parameter is
&selfor&mut self? Its lifetime applies to output references.
You only write explicit lifetimes when these rules aren't enough. Usually functions taking multiple references and returning one of them.
The Static Lifetime
'static means "valid for the entire program." String literals are 'static because they live in the binary itself.
If the compiler suggests 'static as a fix, be suspicious. It usually means your data doesn't live long enough and the real fix is restructuring, not promising the compiler the data lives forever. Slapping 'static on things is the Rust equivalent of casting to void* in C -- it makes the error go away without fixing the problem.
Common Ownership Patterns
String vs. &str
The confusion that hits everyone first. String is owned, heap-allocated, growable. &str is a borrowed view into string data owned by someone else.
Function parameters: &str when you only read. String when the function needs to own. Struct fields: default to String unless you have a specific reason to add a lifetime parameter with &'a str. You probably don't.
Vec Ownership and Struct Ownership
A Vec<String> owns every string it contains. for item in vec consumes it -- elements moved out. for item in &vec borrows immutably. Same for HashMap, BTreeSet, everything.
Structs work identically. Owns its data, drops every field when it goes out of scope:
structServerConfig {
host: String,
port: u16,
allowed_origins: Vec<String>,
}
implServerConfig {
fnnew(host: &str, port: u16) -> Self {
ServerConfig {
host: host.to_string(), // Convert &str to owned String
port,
allowed_origins: Vec::new(),
}
}
fnadd_origin(&mutself, origin: &str) {
self.allowed_origins.push(origin.to_string());
}
fnsummary(&self) -> String {
format!(
"{}:{} with {} origins",
self.host,
self.port,
self.allowed_origins.len()
)
}
}
fnmain() {
let mut config = ServerConfig::new("0.0.0.0", 8080);
config.add_origin("https://example.com");
config.add_origin("https://app.example.com");
println!("{}", config.summary());
}&self borrows immutably. &mut self borrows mutably. Bare self consumes the struct entirely.
Fighting the Borrow Checker
Everyone hits these. Here are the specific errors and the fixes.
Error: "Cannot borrow as mutable because it is also borrowed as immutable"
Holding an immutable reference and trying to mutate:
fnmain() {
let mut data = vec![1, 2, 3, 4, 5];
// BAD: immutable borrow lives while we try to mutate// let first = &data[0];// data.push(6); // ERROR: mutable borrow while immutable exists// println!("{}", first);// FIX 1: Use the immutable ref before the mutable onelet first = &data[0];
println!("First: {}", first); // Immutable borrow ends here
data.push(6); // Now we can mutate -- no conflict!// FIX 2: Clone the value to break the borrow dependencylet first_copy = data[0]; // i32 is Copy, so no borrow at all
data.push(7);
println!("First (copied): {}", first_copy);
}Almost always an ordering problem. End the immutable borrow before the mutable one starts. Non-Lexical Lifetimes help -- borrows end at last use, not at scope end. Moving the println! before the push is enough.
Error: "Does not live long enough"
Trying to return a reference to data created inside a function. The data dies when the function returns. Dangling reference.
Return an owned value (String) instead of a reference (&str). If the data is created inside the function, the function owns it and should transfer ownership to the caller.
Error: "Value used after move"
Already covered above. Clone if you need independent copies. Borrow instead of moving. Or restructure so ownership flows in the right direction.
When to Use Clone
The Rust community treats .clone() like a code smell. Sometimes it is.
But spending an hour threading lifetimes through nested structs to avoid cloning a short config string is a worse smell. Clone when the data is small. Clone when you genuinely need two independent copies. Clone when the alternative -- Rc, Arc, lifetime annotations everywhere -- adds complexity you can't justify. Don't clone inside a hot loop. For one-time setup, parsing, or passing data between modules, .clone() is often the cleanest path and the Rust purists who disagree haven't maintained enough large codebases.
Ownership gets easier after about a month. Not intuitive -- easier. The compiler stops feeling like an adversary and starts feeling like a pair programmer that catches bugs at compile time. Whether that trade-off is worth it depends on what you're building. A CLI tool that processes files? Probably yes. A quick prototype you'll throw away? Maybe not. A networked service handling untrusted input where a single memory bug means a security vulnerability? Absolutely.
Build something small. A file parser, a command-line tool, anything where owned data crosses function boundaries. The concepts only stick when the compiler forces you to apply them, and the errors stop being cryptic once you've seen each one ten times.