Understanding ownership
The model behind every compiler error you have not seen yet. Move, borrow, slice.
Ownership is the thing Rust is famous for. It is also the source of most compiler errors an agent will produce on your behalf. The good news: the model is small. Three rules plus two reference kinds. Once you internalize them, the borrow checker stops feeling adversarial and starts feeling like a reviewer who catches the bugs you would have shipped.
This chapter is the longest in the book on purpose. Read it once, then come back to specific sections when a particular error shows up.
The three rules
These are the entire model. Everything else follows.
- Every value has exactly one owner.
- There is one owner at a time.
- When the owner goes out of scope, the value is dropped. "Dropped" means the destructor runs: memory freed, files closed, locks released.
That is it. The compiler enforces all three at compile time. There is no garbage collector and no runtime overhead; the rules are static.
Stack vs heap, briefly
Rust uses both. The distinction matters because the rules feel different for each.
| Stack | Heap | |
|---|---|---|
| What lives there | Fixed-size, function-local values | Variable-size or long-lived values |
| Cost | Free (just a stack pointer move) | Allocation, bookkeeping, free on drop |
| Examples | i32, bool, f64, [i32; 5], (A, B) | String, Vec<T>, Box<T>, HashMap<K, V> |
String is a stack-resident struct (a pointer, a length, a capacity) pointing at heap-allocated bytes. &str is a pointer plus length on the stack pointing into someone else's bytes. That & is the entire story of borrowing in three characters.
Move: ownership changes hands
For heap-owning types like String, assignment moves ownership.
let s = String::from("hello");
let t = s;
println!("{}", t); // ok
println!("{}", s); // error: value used after moveAfter let t = s;, s is invalid. The bytes on the heap have a new owner. s is not "set to null"; it is statically forbidden. The compiler error is clear.
Why move at all? Because two owners means two destructors run on the same heap allocation. That is a double-free, the kind of bug that ships in C++ and crashes in production. Rust prevents it by giving you exactly one owner.
Passing to a function is also a move:
fn consume(s: String) {
println!("{}", s);
} // s is dropped here
let owned = String::from("hi");
consume(owned);
// owned is no longer validIf you wanted owned to remain usable, you have two choices: pass a borrow (covered next) or clone (covered after).
Copy: stack types are special
Some types implement the Copy trait. Assignment copies the bits instead of moving:
let n: i32 = 5;
let m = n;
println!("{} {}", n, m); // both fineIntegers, booleans, characters, fixed-size arrays of Copy types, and tuples of Copy types all implement Copy. String does not; copying it would mean duplicating the heap allocation, which the language requires you to do explicitly.
The rule of thumb: if a type owns heap memory or any other resource, it is not Copy. If it is purely stack data of fixed size, it usually is.
Clone: explicit deep copy
.clone() produces an independent copy of a value, including any heap memory it owns:
let s = String::from("hello");
let t = s.clone();
println!("{} {}", s, t);Both s and t now own independent heap allocations. Both will be dropped at scope end.
.clone() is explicit because it is rarely cheap. A String::clone is an allocation and a memcpy. A Vec<T>::clone allocates and copies every element. Most of the time, what you actually want is to share access without duplicating, which is what borrowing is for.
Borrow: read access without ownership
&value produces a shared (immutable) reference. The owner keeps ownership; the reference holds a temporary view.
fn length(s: &String) -> usize {
s.len()
}
let s = String::from("hello");
let n = length(&s);
println!("{} {}", s, n); // s is still validThe function takes &String, which says "I want to read this without owning it." The caller keeps s and uses it after the call.
You can have as many shared references as you want, simultaneously:
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{} {} {}", r1, r2, r3); // fineMutable borrow: temporary write access
&mut value produces an exclusive (mutable) reference. The owner gives up access for the duration of the borrow.
fn push_exclamation(s: &mut String) {
s.push('!');
}
let mut s = String::from("hello");
push_exclamation(&mut s);
println!("{}", s); // "hello!"The crucial rule:
At any point, you have either many shared references or one exclusive reference. Never both.
let mut s = String::from("hello");
let r1 = &s; // shared
let r2 = &s; // also shared, fine
let r3 = &mut s; // error: cannot borrow as mutable while shared refs existThis rule is what makes data races impossible at compile time. If you have an exclusive reference, no one else can read or write. If many readers exist, no one can write. The compiler knows.
The references' scopes are "non-lexical": a shared borrow ends at its last use, not at the end of its block. So this works:
let mut s = String::from("hello");
let r = &s;
println!("{}", r); // last use of r
let m = &mut s; // fine, r is no longer live
m.push('!');Dangling references: prevented at compile time
A dangling reference points at memory that was already freed. In C and C++, this is a famous source of crashes and security bugs. In Rust, it does not compile.
fn dangle() -> &String {
let s = String::from("hello");
&s
}
// s is dropped at the end of the function; the returned reference would point at freed memory.The compiler refuses, demanding a lifetime annotation it cannot satisfy. The fix is either to return the owned String (move it out) or to take a reference parameter the return can be tied to.
This guarantee, "if it compiles, no use-after-free," is what makes Rust uniquely valuable for systems where crashes have real costs. It is also why agent-written Rust that compiles is much more trustworthy than agent-written C.
Slices: borrowed views into collections
A slice is a reference plus a length. Two common ones:
let s = String::from("hello world");
let hello: &str = &s[0..5];
let world: &str = &s[6..11];
let v = vec![1, 2, 3, 4, 5];
let mid: &[i32] = &v[1..4]; // [2, 3, 4]&str is "a borrowed view into UTF-8 bytes." &[T] is "a borrowed view into a sequence of T." Both are pointer-plus-length; both live on the stack.
The orchestrator's rule for function signatures:
- If the function reads a string, take
&str, not&String.&straccepts bothStringand&strcallers. - If the function reads a sequence, take
&[T], not&Vec<T>.&[T]accepts arrays, vectors, and slices. - If the function needs ownership (will store the value, mutate it, or return parts of it), take the owned type and document why.
fn count_words(text: String) -> usize {
text.split_whitespace().count()
}
fn sum(numbers: Vec<i32>) -> i32 {
numbers.iter().sum()
}fn count_words(text: &str) -> usize {
text.split_whitespace().count()
}
fn sum(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}The right side takes borrows. Callers do not give up their data. The signatures accept more inputs (literal strings, slices, vectors). No allocations to satisfy the signature. These are the kinds of changes worth pushing back on in code review.
Why agents reach for .clone()
The pattern is consistent. The agent writes a function. It hits "value used after move." It tries .clone(). The code compiles. The agent claims success.
The cost: every .clone() is an allocation in a hot path, every time. Across a real codebase, this adds up to measurable performance loss and a code shape that future readers will struggle to refactor.
The right fix, ninety percent of the time:
- Change the function to take a borrow instead of an owned value.
- If the function genuinely needs ownership, ask the caller to give up its copy or use
Arc<T>for shared ownership. - If you do clone, clone at the boundary where ownership is given away, not deep inside a loop.
Lifetimes: a teaser
References carry lifetimes: the scope during which they are valid. The compiler infers them in almost all cases. You rarely write 'a yourself.
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() >= s2.len() { s1 } else { s2 }
}'a says: "the returned reference is valid for as long as both inputs are." Without it, the compiler cannot tell which input the output came from. With it, the borrow check works.
When you see 'static, that means "valid for the entire program." Be skeptical when it appears in agent code. The legitimate uses are string literals (&'static str) and intentionally leaked memory (Box::leak). Random 'statics in function signatures usually signal an unresolved lifetime problem the agent worked around.
The full lifetime story is its own chapter. For now: know the syntax exists, prefer code where the compiler elides it, and treat manual 'static as a smell.
Putting it together
The mental model for any value:
- Where does it live? Stack if fixed size and
Copy-shaped, heap if it owns growable data. - Who owns it? Trace the path from creation to drop.
- How is it shared? Borrow (
&Tor&mut T) for temporary access, clone for independent copies,Arc<T>for cheap shared immutable access,Arc<Mutex<T>>for shared mutable across threads.
When you read an agent's Rust diff, run those three questions on each new variable. Most bugs and most over-allocations fall out of one of them.