Ownership in One Page
The rule
Rust has exactly three ownership rules. You will read them in twenty seconds and spend the rest of the week getting used to them.
- Every value has exactly one owner.
- When the owner goes out of scope, the value is dropped (memory freed, file closed, lock released).
- Ownership can be moved from one name to another, but it cannot be in two places at once.
The library card analogy
One library card. One holder. At all times.
The code that shows move
fn main() {
let ada = String::from("book");
let ben = ada; // ownership moves to ben
println!("{ben}"); // fine
// println!("{ada}"); // would not compile
}
▶ Run this in the Rust Playground
If you uncomment the last line, Rust says:
error[E0382]: borrow of moved value: `ada`
That is the whole behavior. Read it as “Ada gave the card to Ben. She does not have it anymore.”
If this feels restrictive — good, you're reading it right. It is restrictive. In exchange, you get a language where "use after free", "double free", and "data race" are compile errors, not 3 a.m. pages. That is the deal.
Why some values “copy” instead of “move”
Try this and it works fine:
fn main() {
let a = 5;
let b = a;
println!("{a} {b}"); // both are valid
}
▶ Run this in the Rust Playground
Integers, booleans, floats, and a few other small types are Copy. Copying them is cheap and has no cleanup cost. Rust doesn’t move them — it duplicates them bit-for-bit and everyone is happy.
String, Vec<T>, HashMap, file handles, locks — none of these are Copy. They own a resource (heap memory, a file descriptor, a lock) that has cleanup cost. Rust refuses to silently duplicate them.
If it's a plain number or a small fixed-size value, it copies. If it owns memory on the heap, a file, or a connection, it moves. When in doubt, assume move.
What “scope” means
A value’s scope is the region of code where its owner is visible. Usually that is a block — the { ... } it was declared in. When that block ends, Rust calls drop on the value.
fn main() {
{
let msg = String::from("hello");
println!("{msg}");
} // msg is dropped here. its memory is freed, right now, for sure.
// println!("{msg}"); // would not compile; msg is out of scope
}
drop is deterministic. Not garbage-collected. Not “sometime later”. The moment the block ends — that is when the cleanup happens.
Rust does not have a garbage collector. It does not need one. The compiler already knows exactly when to free every value — at the end of the scope that owns it.
Now watch the library card change wallets. This is the whole chapter in one animation.
Interactive simulation (requires JavaScript): assigning ada to ben moves ownership of the heap buffer without copying it; using ada afterwards fails with E0382 because two owners would free the same memory twice.
- Write
let s1 = String::from("hi"); let s2 = s1;and try to prints1. Read the error. - Change
let s1 = String::from("hi");tolet s1 = 42;and rerun. Why does it work now? - Put a
Stringinside a block{ }and try to use it after the block ends. Read the error.
Ownership says “only one owner at a time”. But often we need to look at a value without taking it over. That is borrowing — the next chapter.