Ownership 👑
This is it: the idea that made Rust famous, the reason it needs no garbage collector, and the reason it can't crash from memory bugs. Take this lesson slow. It's worth it.
The problem every language must solve
Programs constantly create data in memory. Eventually that memory must be given back, or the program bloats and dies. Before Rust, there were two options:
- You free memory by hand (C, C++): fast, but humans forget. Free too late: memory leak. Free too early and use it anyway: crash, or a security hole. Remember the home page stat: ~70% of serious security bugs are exactly this.
- A garbage collector cleans up (Python, Java, JavaScript, Go): safe, but a background janitor costs memory and can pause your program at the worst moment.
Rust found a third way: the compiler figures out, at compile time, the exact moment each value is no longer needed, and frees it there. No janitor. No human error. To pull that off, Rust needs one simple system of rules. That system is ownership.
The three rules 📜
- Every value has exactly one owner (a variable).
- When the owner goes out of scope, the value is dropped (freed).
- Assigning or passing the value moves ownership to the new place.
Rule 2 in action: scope is destiny
A scope is a region between braces { }. When a variable's
scope ends, its value is cleaned up, instantly and predictably:
fn main() {
{
let toy = String::from("squeaky crab");
println!("playing with {toy}");
} // ← toy's scope ends HERE: memory freed, automatically
// println!("{toy}"); // ❌ toy doesn't exist anymore
}
That's the entire garbage collector replacement: a closing brace.
Rule 3 in action: the move 📦➡️
Here's the moment that surprises everyone. Watch closely:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // ownership MOVES from s1 to s2
println!("{s2}"); // ✅ fine, s2 owns it now
println!("{s1}"); // ❌ compile error!
}
error[E0382]: borrow of moved value: `s1`
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`
3 | let s2 = s1;
| -- value moved here
5 | println!("{s1}");
| ^^^^ value borrowed here after move
Why?! Think of the String as a balloon 🎈 and variables as hands.
Rule 1 says only one hand may hold the string at a time. let s2 = s1;
passes the balloon to the other hand. If Rust let both hands believe they
own it, both would try to pop it at scope's end: a double free, one of
the classic C crashes. Rust's answer: the old hand simply lets go. s1
is done, and using it is a compile error, not a 3am production incident.
Want two balloons? clone
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // an actual second copy, in new memory
println!("{s1} and {s2}"); // ✅ both valid: two values, two owners
}
.clone() is honest, safe, and fine. Optimizing away clones is a
hobby for later. Correct first, clever second.
Small stuff just copies 🍬
Numbers, booleans, and chars are so small they live entirely on the stack (fast, fixed-size memory), so Rust copies them instead of moving:
fn main() {
let a = 5;
let b = a; // a plain copy, no move drama
println!("{a} and {b}"); // ✅ both fine
}
Why the difference? A String keeps its text in heap memory
(the big, flexible warehouse), and only a claim ticket lives in the variable.
Copying tickets without copying the goods is exactly the double-free trap, so
heap-backed values move. Simple values with no warehouse claim
(i32, bool, char, f64…) are
"Copy types": they just duplicate.
Functions move things too
Passing a value into a function follows the same rule; the parameter becomes the new owner:
fn devour(snack: String) {
println!("nom nom: {snack}");
} // snack's scope ends → the String is freed here
fn main() {
let cake = String::from("carrot cake");
devour(cake); // ownership moves into the function...
// println!("{cake}"); // ❌ ...so cake is gone out here
}
Giving a value away every time you call a function would be exhausting, though. What if you could lend the cake without giving it up? That's exactly what Lesson 7 is about, and it's where ownership starts feeling natural instead of strict.
- Using a variable after assigning it elsewhere: "value borrowed here after
move." Fix:
.clone(), or read on to Lesson 7 and lend instead. - Expecting numbers to behave like Strings: they don't; they're Copy types.
- Trying to memorize the memory theory before feeling it. Do the exercises; hands teach faster than diagrams.
Predict the verdict
Which of these compiles? Decide first, then test both in the Playground.
// Program A
let word = String::from("crab");
let copy = word;
println!("{word}");
// Program B
let num = 42;
let copy = num;
println!("{num}");
Reveal answer
A fails: String moves, so word is
invalid after the assignment. B compiles: i32 is a
Copy type; copy is a duplicate, and num lives on.
Two fixes, your choice
Make this compile two different ways: once with clone, once by
reordering the code so the move happens last.
fn main() {
let motto = String::from("stay rusty");
let banner = motto;
println!("original: {motto}");
println!("banner: {banner}");
}
Reveal solution
// Fix 1: clone
let motto = String::from("stay rusty");
let banner = motto.clone();
println!("original: {motto}");
println!("banner: {banner}");
// Fix 2: use motto BEFORE moving it
let motto = String::from("stay rusty");
println!("original: {motto}");
let banner = motto;
println!("banner: {banner}");
The vanishing cake
Type out the devour example from above and confirm the error.
Then change devour to return the String
(fn devour(snack: String) -> String) and hand ownership back with
let cake = devour(cake);. Clunky? Extremely. Remember this feeling;
Lesson 7 exists to fix it.
Reveal solution
fn devour(snack: String) -> String {
println!("nom nom: {snack}");
snack // hand it back (expression, no semicolon!)
}
fn main() {
let cake = String::from("carrot cake");
let cake = devour(cake); // ownership out, ownership back
println!("{cake} survived!");
}