Ownership basics
Rust's ownership system — the rule that gives you memory safety without a garbage collector — explained from the stack up.
- Explain what the stack and heap are and which Rust types use each
- State Rust's core ownership rule and the three sub-rules that follow from it
- Describe what "move semantics" means and why moved values can no longer be used
- Use clone() to make an explicit deep copy when you need to keep both values
- Understand what the borrow checker is and what it protects against
Every programming language has to answer the same question: when you're done with a piece of memory, how does the program reclaim it? Garbage-collected languages (Python, JavaScript, Go) scan for unreachable values at runtime. Languages like C leave it entirely to you — and you will sometimes forget. Rust takes a third path: the compiler tracks ownership statically, at compile time, and inserts the frees for you. No runtime cost, no dangling pointers.
This is the idea that makes Rust unique. It's also the idea that makes the compiler push back on you more than you might expect. This lesson explains the core rule; the rest of the language clicks into place once you have it.
Stack and heap: a quick mental model
The fundamentals track covers memory in detail. A quick refresher for Rust's purposes:
- The stack is fast, fixed-size storage. Every local variable of a known, fixed size lives here. When a function returns, its entire stack frame disappears automatically.
- The heap is slower, dynamically-sized storage. Data whose size isn't known at compile time — like a
Stringthat can grow — lives on the heap.
Rust's scalar types (i32, f64, bool, char) are small and fixed-size, so they live on the stack. A String stores its text on the heap and keeps a pointer (plus length and capacity) on the stack.
This distinction matters because heap data doesn't automatically disappear when a function returns — it has to be explicitly freed. Ownership is how Rust automates that.
The ownership rule
Rust enforces one central rule:
Each value has exactly one owner. When the owner goes out of scope, the value is dropped.
"Dropped" means the memory is freed. No garbage collector needed — the compiler inserts the drop at the end of the owning scope.
Two important consequences:
- There can only be one owner at a time. You cannot have two variables that both own the same heap allocation.
- Ownership can be transferred. When you do, the original owner can no longer be used.
Move semantics
This is where new Rust programmers usually hit their first real compiler error. Try this:
let s1 = String::from("hello");
let s2 = s1; // ownership moves to s2
println!("{}", s1); // compiler error!error[E0382]: borrow of moved value: `s1`
--> src/main.rs:4:20
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`
3 | let s2 = s1;
| -- value moved here
4 | println!("{}", s1);
| ^^ value borrowed here after moveAfter let s2 = s1, ownership of the String has moved to s2. Using s1 after that is a compile error. There is still only one owner (s2) and when s2 goes out of scope, the String will be freed exactly once.
This might feel strange at first. In Python, s2 = s1 would give you two names pointing at the same object. In Rust, s1 is gone — the compiler prevents you from using a moved value.
Why no shallow copy? If let s2 = s1 made a shallow copy, you'd have two owners of the same heap data. When both went out of scope, both would try to free the same memory — a "double free" bug, which leads to crashes and security vulnerabilities. Rust makes it a compile error instead.
Copy types: scalars are different
Stack-only types implement a trait called Copy. For them, assignment makes a bitwise copy — no move, no ownership transfer:
let x = 5;
let y = x; // x is copied, not moved
println!("{}", x); // still valid!i32, f64, bool, char, and tuples of Copy types all implement Copy. String does not — strings are heap-allocated, so assignment is a move.
clone() — explicit deep copy
When you genuinely need two independent owned String values, call .clone():
let s1 = String::from("hello");
let s2 = s1.clone(); // deep copy (new allocation, new owner)
println!("s1={}, s2={}", s1, s2); // both valid.clone() makes a deep copy — a completely new heap allocation with the same content. It's intentionally verbose: a signal that says "this costs memory." That's the kind of cost you want to see, not hide.
Don't reach for clone() to fix every borrow error. It works, but it's often not the right answer — it allocates. The idiomatic solution is usually borrowing (covered in the intermediate track). For now, use clone() when you understand that you want two separate copies.
The borrow checker
The ownership rule is enforced by the borrow checker, the part of the compiler that tracks who owns what. When you see an error like value moved here or cannot borrow x as mutable because it is also borrowed as immutable, that's the borrow checker.
The borrow checker can feel adversarial at first. Experienced Rust programmers describe the experience differently: the compiler is finding a real problem in your mental model. Fighting it less and reading its messages more closely is the fastest path forward.
Ownership and functions
Passing a String to a function moves ownership into the function:
fn print_greeting(s: String) {
println!("Hello, {}", s);
} // s is dropped here
fn main() {
let name = String::from("Ada");
print_greeting(name);
// name is no longer valid here — it was moved
}This is surprising but consistent: the same ownership rule applies everywhere. The intermediate track covers references (&String) — a way to let functions look at a value without taking ownership of it. That's borrowing, and it's the idiomatic solution for most cases.
Check your understanding
Knowledge check
- 1.What happens when the owner of a value goes out of scope in Rust?
- 2.After
let s2 = s1;where s1 is a String, what happens if you use s1? - 3.Which of the following types implement Copy in Rust?
- 4.Calling .clone() on a String allocates new heap memory.
When the agent's away
The borrow checker errors you'll encounter most often:
use of moved value— you used a variable after its value was moved somewhere else. Eitherclone()before the move, or redesign to avoid needing both.cannot borrow as mutable— you're trying to mutate something that isn'tmut, or that has active borrows. Check formutfirst; borrow issues will make more sense after the intermediate track.does not live long enough— a reference is trying to outlive the value it points to. The compiler is preventing a dangling pointer.
Read the error, look at the line numbers it points to, and trust that the compiler is right about the problem even when it isn't immediately obvious why.
Where to go next
Ownership is the central idea of Rust. Everything else — references, lifetimes, smart pointers — builds on this foundation. Before going further, practice: write a few small programs that move values between functions, observe the errors, and fix them. Next: the lab puts the whole core syntax module together.