Ownership & Borrowing
What is Ownership?
Section titled “What is Ownership?”Rust’s most distinctive feature is its ownership system — a set of rules the compiler enforces at compile time to guarantee memory safety without a garbage collector.
Every piece of memory in a Rust program is owned by exactly one variable at any given moment. When that variable goes out of scope, Rust automatically frees the memory. There is no runtime bookkeeping, no GC pauses, and no possibility of use-after-free or double-free bugs — the compiler rules them out entirely.
The Three Rules of Ownership
Section titled “The Three Rules of Ownership”- Every value in Rust has exactly one owner.
- When the owner goes out of scope, the value is dropped (memory is freed).
- Ownership can be transferred (moved) to a new owner — at which point the old owner is no longer valid.
These three rules are the entire foundation. Everything else in this module — moves, references, mutable borrows, and slices — flows from them.
fn main() { // Rule 1: one owner let s = String::from("hello");
// Rule 2: out of scope → dropped { let inner = String::from("inner"); println!("{}", inner); } // inner is freed here
// Rule 3: ownership transferred to t let t = s; println!("{}", t); // s is no longer valid here}What This Module Covers
Section titled “What This Module Covers”| Lesson | Concept |
|---|---|
| Ownership & Moves | Each value has one owner; assignment moves non-Copy types |
| References & Borrowing | Borrow a value with &T without taking ownership |
| Mutable Borrows | Modify through &mut T; the exclusive-access rule |
| Slices | Borrow a contiguous portion of a collection with &[T] or &str |
| Smart Pointers | Box, Rc/Weak, RefCell, Cell, Arc — heap allocation, shared ownership, interior mutability |
Why No Garbage Collector?
Section titled “Why No Garbage Collector?”A GC scans memory at runtime to find unreachable values and free them. This adds runtime overhead and introduces unpredictable pauses. Rust moves that work entirely to compile time: the compiler tracks every ownership transfer and scope exit and inserts the drop calls for you. The result is C-level performance with complete memory safety.
fn main() { // Rule 1: every value has exactly one owner let owner = String::from("owned by owner"); println!("Rule 1: {}", owner);
// Rule 2: when the owner goes out of scope, the value is dropped { let scoped = String::from("scoped value"); println!("Rule 2 inside scope: {}", scoped); } // scoped is dropped here — memory is freed println!("Rule 2: scoped is gone");
// Rule 3: ownership can be transferred (moved) let new_owner = owner; println!("Rule 3: now {}", new_owner); // 'owner' is no longer valid here}Compiling…
Which Chapter Do I Need?
Section titled “Which Chapter Do I Need?”Every line of Rust code that touches a value is really answering one question: what do I need to do with this value right now? The answer routes you to one of the four lessons in this module.
flowchart TD
Q{What do you need from the value?}
Q -->|Give it away for good| MOVE[Move ownership - this lesson, Ownership and Moves]
Q -->|Just read it| REF[Shared reference, read-only - References and Borrowing]
Q -->|Change it in place| MUT[Exclusive reference, mutable - Mutable Borrows]
Q -->|Only need part of a collection| SLICE[Borrowed view - Slices] If you are not sure yet, the default in idiomatic Rust is almost always borrow first (&T, or &mut T if you must mutate) and reach for a move only when the callee genuinely needs to own the value afterward — for example, storing it in a struct or sending it to another thread.
What the Compiler Says: Reusing a Moved Value
Section titled “What the Compiler Says: Reusing a Moved Value”The single most common first error every Rust learner hits is using a value after it has moved. Here it is with a Vec<i32> instead of a String, to show the rule is about the type’s Copy-ness, not about strings specifically:
// @expect-error E0382fn main() { let v1 = vec![1, 2, 3]; let v2 = v1; // v1 moves into v2 — Vec<i32> is not Copy println!("{:?}", v1); // ERROR: v1 was moved println!("{:?}", v2);}error[E0382]: borrow of moved value: `v1` --> src/main.rs:4:22 |2 | let v1 = vec![1, 2, 3]; | -- move occurs because `v1` has type `Vec<i32>`, which does not implement the `Copy` trait3 | let v2 = v1; // v1 moves into v2 — Vec<i32> is not Copy | -- value moved here4 | println!("{:?}", v1); // ERROR: v1 was moved | ^^ value borrowed here after move |help: consider cloning the value if the performance cost is acceptable |3 | let v2 = v1.clone(); // v1 moves into v2 — Vec<i32> is not Copy | ++++++++Notice the compiler names the exact rule: Vec<i32> “does not implement the Copy trait.” The next lesson, Ownership & Moves, is entirely about which types do and why that distinction exists.
Mental Model
Section titled “Mental Model”Think of ownership as a single-owner tree, not a web of pointers. At every instant, each value has exactly one variable that is responsible for freeing it — moving a value re-roots that responsibility onto a new variable, and it never duplicates. Borrowing (&T and &mut T) does not touch this tree at all — it just opens a temporary, compiler-checked window onto a value someone else still owns, and the window is guaranteed to close before the owner does.
Trade-offs
Section titled “Trade-offs”Compile-time ownership checking has a real cost: it is more upfront mental effort than a garbage-collected language, and the compiler will reject some patterns that are actually safe but that it cannot prove safe (shared mutable graphs are the classic example — Rust pushes those into Rc/RefCell, covered later in the course). What you get in exchange is zero runtime overhead for memory safety — no GC pause, no reference-counting tax by default — and a whole category of bugs (use-after-free, double-free, data races on shared data) that simply cannot compile.
Common Misconceptions
Section titled “Common Misconceptions”- “Moving a value copies it, like assignment does in most other languages.” The opposite is true by default: assignment moves non-
Copyvalues and invalidates the original. Only types that explicitly implementCopyget a bitwise duplicate instead — the next lesson covers exactly which types qualify and why. - “The three rules mean Rust is silently copying data behind my back to stay safe.” No hidden copies happen. The compiler either transfers ownership (cheap: it copies a small fixed-size header, never the underlying heap data) or lets you borrow — an explicit
.clone()is the only way to get a real duplicate of the data. - “Once I understand the
&syntax, I understand ownership.” Syntax is the easy part. The rule that takes longest to internalize is the exclusivity invariant enforced across an entire function body — that a value can have many readers or one writer, never both — which the Mutable Borrows lesson covers in depth.
Reading AI-generated code
Section titled “Reading AI-generated code”AI assistants very often write function signatures that take ownership (s: String) when the function body only ever reads the value. This compiles fine, so nothing flags it automatically — but it forces every caller to either move their own value away or add an unnecessary .clone().
🐛 Spot the bug
fn print_summary(title: String) { println!("Summary: {}", title);}
fn main() { let title = String::from("Quarterly Report"); print_summary(title); // println!("{}", title); // would not compile — title was moved for no reason}Show the answer
print_summary never mutates or stores title — it only reads it once to print. Taking String by value forces the caller to give up ownership (or clone) just to make one read-only call. The fix, covered in the next lesson, is to take a reference instead: fn print_summary(title: &str), called as print_summary(&title) — the caller keeps title usable afterward, and no heap data is copied at all.