Skip to content

Ownership & Borrowing

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.

  1. Every value in Rust has exactly one owner.
  2. When the owner goes out of scope, the value is dropped (memory is freed).
  3. 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
}
LessonConcept
Ownership & MovesEach value has one owner; assignment moves non-Copy types
References & BorrowingBorrow a value with &T without taking ownership
Mutable BorrowsModify through &mut T; the exclusive-access rule
SlicesBorrow a contiguous portion of a collection with &[T] or &str
Smart PointersBox, Rc/Weak, RefCell, Cell, Arc — heap allocation, shared ownership, interior mutability

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
}

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]
Ownership decision flow

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 E0382
fn 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` trait
3 | let v2 = v1; // v1 moves into v2 — Vec<i32> is not Copy
| -- value moved here
4 | 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.

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.

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.

  • “Moving a value copies it, like assignment does in most other languages.” The opposite is true by default: assignment moves non-Copy values and invalidates the original. Only types that explicitly implement Copy get 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.

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.

How many owners can a value have at one time in Rust?
What happens to a value when its owner goes out of scope?
Which mechanism does Rust use to guarantee memory safety?
Does this compile? What happens if you try?
fn main() {
    let a = vec![1, 2, 3];
    let b = a;
    println!("{:?}", a);
}
What does this program print?
fn main() {
    let x = 5;
    let y = x;
    println!("{} {}", x, y);
}