In the previous tutorial, we learned variables, types, and functions. Now we tackle the most important concept in Rust — ownership.
Ownership is what makes Rust unique. It is the reason Rust has no garbage collector, yet never leaks memory. Every Rust programmer must understand ownership. Once you get it, the rest of Rust clicks into place.
Why Ownership Exists
Most languages manage memory in one of two ways:
- Garbage collector (Java, Kotlin, Go, Python) — A background process finds and frees unused memory. Simple for the programmer, but uses extra CPU and can cause pauses.
- Manual management (C, C++) — The programmer allocates and frees memory. Fast, but easy to make mistakes — use-after-free, double-free, memory leaks.
Rust takes a third approach:
- Ownership — The compiler tracks who owns each piece of data. When the owner goes out of scope, the data is freed automatically. No garbage collector. No manual free. No bugs.
The cost? You must follow ownership rules. The compiler enforces them. At first this feels strict, but it catches real bugs at compile time.
The Three Ownership Rules
Memorize these. Everything else follows from them.
- Each value has exactly one owner — a variable that owns the data.
- When the owner goes out of scope, the value is dropped — memory is freed automatically.
- Assignment transfers ownership — the old variable can no longer use the data.
Let’s see each rule in action.
Stack vs Heap — Where Data Lives
Before we go deeper, you need to understand where data lives in memory.
┌─────────────────────────────────┐
│ STACK │
│ (fast, fixed-size, automatic) │
│ │
│ ┌───────────┐ │
│ │ age: 25 │ ← i32 (4 bytes)│
│ ├───────────┤ │
│ │ active: T │ ← bool (1 byte)│
│ ├───────────┤ │
│ │ ptr ──────┼──┐ │
│ │ len: 4 │ │ │
│ │ cap: 4 │ │ │
│ └───────────┘ │ │
└─────────────────┼───────────────┘
│
┌─────────────────┼───────────────┐
│ HEAP ▼ │
│ (slower, dynamic, flexible) │
│ │
│ ┌───┬───┬───┬───┐ │
│ │ R │ u │ s │ t │ │
│ └───┴───┴───┴───┘ │
│ │
└─────────────────────────────────┘
Stack: Fast. Fixed size. Values like integers, booleans, and floats live here. When a function ends, its stack data is gone instantly.
Heap: Slower. Dynamic size. Values like String and Vec store their data here. A pointer on the stack points to the actual data on the heap.
Why does this matter? Copying stack data is cheap — just copy a few bytes. Copying heap data is expensive — you must allocate new memory and copy all the bytes. This difference is why Rust has move and copy semantics.
Move Semantics — Ownership Transfers
When you assign a String to another variable, ownership moves. The original variable becomes invalid.
fn main() {
let name = String::from("Alex");
let other_name = name; // Ownership moves to other_name
// println!("{}", name); // ERROR: value used after move
println!("{}", other_name); // OK — other_name owns the data
}
What happens in memory:
BEFORE move:
Stack Heap
name: ──────────────► [ A | l | e | x ]
AFTER move:
Stack Heap
name: (invalid)
other_name: ─────────► [ A | l | e | x ]
The heap data does not move. Only the pointer on the stack changes hands. The old variable name is marked invalid. If you try to use it, the compiler stops you.
Why Move Instead of Copy?
Imagine if both name and other_name pointed to the same heap data. When name goes out of scope, it frees the memory. Then when other_name goes out of scope, it tries to free the same memory again — a double free bug. This is a real security vulnerability in C and C++.
Rust prevents this by making moves the default for heap data. One owner. One free. No bugs.
The Library Book Analogy
Think of ownership like a library book:
- A book can only be checked out by one person at a time.
- When you give the book to someone else, you no longer have it.
- When the person who has it leaves the library, the book goes back on the shelf (memory is freed).
fn main() {
let book = String::from("Rust Programming");
// Alex gives the book to Sam
let alex_book = book;
// Alex can't read it anymore — Sam has it
// Sam gives it to Jordan
let sam_book = alex_book;
// Sam can't read it anymore — Jordan has it
println!("Jordan reads: {}", sam_book); // Only Jordan can use it
}
Copy Trait — When Values Copy Instead of Move
Not all types move. Simple types that live entirely on the stack copy instead:
fn main() {
let age = 25;
let other_age = age; // Copy, not move!
println!("age: {}", age); // OK — still valid
println!("other_age: {}", other_age); // OK — independent copy
}
Why? Copying an integer is just copying 4 bytes on the stack. It is so cheap that moving would be pointless.
Types That Implement Copy
These types copy automatically:
| Type | Why it copies |
|---|---|
i32, u64, all integers | Small, fixed-size, stack-only |
f32, f64 | Small, fixed-size, stack-only |
bool | 1 byte |
char | 4 bytes |
| Tuples of Copy types | (i32, bool) copies, (i32, String) does NOT |
Types That Move
These types move on assignment:
| Type | Why it moves |
|---|---|
String | Owns heap data |
Vec<T> | Owns heap data |
Box<T> | Owns heap data |
| Any struct with non-Copy fields | Contains owned data |
The rule: If a type manages heap memory, it moves. If it lives entirely on the stack, it copies.
Clone — Explicit Deep Copy
What if you DO want to copy heap data? Use .clone():
fn main() {
let name = String::from("Alex");
let other_name = name.clone(); // Deep copy — new heap allocation
println!("name: {}", name); // OK — still valid
println!("other_name: {}", other_name); // OK — independent copy
}
What happens in memory:
AFTER clone:
Stack Heap
name: ──────────────► [ A | l | e | x ]
other_name: ─────────► [ A | l | e | x ] (separate copy)
Now there are two independent String values. Each owns its own heap data. Each will be freed separately.
Use .clone() when you need two independent copies. But be aware — cloning is expensive for large data. Rust makes you write .clone() explicitly so you see the cost.
Ownership and Functions
Passing a value to a function works like assignment — it transfers ownership.
fn print_name(name: String) {
println!("Name: {}", name);
// name is dropped here — memory freed
}
fn main() {
let user = String::from("Alex");
print_name(user); // Ownership moves to print_name
// println!("{}", user); // ERROR: user was moved
}
This is a common surprise for beginners. After calling print_name(user), the variable user is no longer valid.
main() print_name()
┌──────────┐ ┌──────────┐
│ user ────┼── moves to ──► name │
│ (invalid)│ │ │
└──────────┘ └──────────┘
name dropped at }
Copy Types Pass Without Moving
Just like assignment, Copy types are copied when passed to functions:
fn double(x: i32) -> i32 {
x * 2
}
fn main() {
let number = 42;
let result = double(number);
println!("number: {}", number); // OK — i32 copies
println!("result: {}", result);
}
Return Values Transfer Ownership Back
A function can give ownership back through its return value:
fn create_greeting(name: &str) -> String {
format!("Hello, {}!", name)
}
fn main() {
let greeting = create_greeting("Alex");
println!("{}", greeting); // greeting owns the String
}
You can also take ownership and give it back:
fn add_exclamation(mut text: String) -> String {
text.push('!');
text // Return ownership back to caller
}
fn main() {
let message = String::from("Hello");
let message = add_exclamation(message); // Move in, move back
println!("{}", message); // Hello!
}
This works but is clunky. In the next tutorial, we will learn borrowing — a way to let functions use data without taking ownership.
Scope and Drop
When a variable goes out of scope, Rust calls drop automatically:
fn main() {
{
let name = String::from("Alex");
println!("{}", name);
} // name goes out of scope — drop is called, memory freed
// println!("{}", name); // ERROR: name doesn't exist here
}
This is deterministic. You always know exactly when memory is freed — at the closing }. No garbage collector deciding when to clean up.
Drop Order
Variables are dropped in reverse order of creation:
fn main() {
let first = String::from("created first");
let second = String::from("created second");
// At end of main:
// second is dropped first
// first is dropped second
}
Common Compiler Errors
Error: Use of Moved Value
let name = String::from("Alex");
let other = name;
println!("{}", name); // ERROR!
error[E0382]: borrow of moved value: `name`
--> src/main.rs:4:20
|
2 | let name = String::from("Alex");
| ---- move occurs because `name` has type `String`
3 | let other = name;
| ---- value moved here
4 | println!("{}", name);
| ^^^^ value borrowed here after move
Fix: Use .clone() if you need both, or use the new variable instead.
Error: Use After Move to Function
fn consume(s: String) { }
fn main() {
let data = String::from("hello");
consume(data);
println!("{}", data); // ERROR!
}
error[E0382]: borrow of moved value: `data`
--> src/main.rs:6:20
|
4 | let data = String::from("hello");
| ---- move occurs because `data` has type `String`
5 | consume(data);
| ---- value moved here
6 | println!("{}", data);
| ^^^^ value borrowed here after move
Fix: Have the function return the value, clone before passing, or use references (next tutorial).
Error: Partial Move
let pair = (String::from("hello"), String::from("world"));
let first = pair.0; // Moves first element
println!("{:?}", pair); // ERROR: pair partially moved
Fix: Clone the element, or use references.
Summary
| Concept | What Happens | Example |
|---|---|---|
| Move | Ownership transfers, old var invalid | let b = a; (String) |
| Copy | Value duplicated, both vars valid | let b = a; (i32) |
| Clone | Explicit deep copy, both vars valid | let b = a.clone(); |
| Drop | Memory freed when owner leaves scope | } |
| Function pass | Same as assignment (move or copy) | func(value) |
| Function return | Ownership transferred to caller | let x = func(); |
Source Code
Related Tutorials
- Rust Tutorial #3: Variables, Types, and Functions — the building blocks
- Rust Tutorial #5: Borrowing and References — the other half of ownership
What’s Next?
Ownership by itself is limiting. You don’t always want to move data around. In the next tutorial, we learn borrowing and references — how to let functions read or modify data without taking ownership. This is the other half of Rust’s memory model.