HOW-TO: Rust Ownership and Borrowing
Master Rust's most distinctive feature: the ownership system. Learn how Rust manages memory without garbage collection, why moves happen, how borrowing solves the problem, and the borrowing rules that prevent data races at compile time.
HOW-TO: Rust Ownership and Borrowing
Overview
Ownership is Rust's defining feature. It's also the hardest concept for newcomers to grasp. But once you understand it, everything else in Rust becomes clearer.
This guide walks you through:
- Why ownership exists (memory safety without garbage collection)
- The three ownership rules (simple but powerful)
- Move semantics (why
let x = ybehaves differently than you expect) - Borrowing and references (how to use data without owning it)
- The borrowing rules (prevent data races at compile time)
Warning: This is harder than "Getting Started." Take your time. Try the code. Make mistakes. That's how Rust clicks.
Why Ownership Matters
Most programming languages handle memory one of two ways:
-
Garbage Collection (Python, Java, Go)
- The language automatically cleans up unused memory
- You don't think about it, but cleanup runs during execution (can cause pauses)
-
Manual Management (C, C++)
- You explicitly allocate and free memory
- Fast, but error-prone (use-after-free, memory leaks, double-free)
Rust uses a third way: Ownership.
Memory is managed through a set of rules the compiler checks. If you violate the rules, your program won't compile. If it compiles, it's safe.
The benefits:
- No garbage collection — no runtime pauses
- No manual memory management — no leaks or use-after-free bugs
- Memory safety at compile time — errors caught before they happen
The cost: You have to learn a new way of thinking about data ownership.
The Stack vs. The Heap
To understand ownership, you need to understand memory layout. Rust cares about where your data lives.
The Stack
Think of a stack of plates:
- You add plates to the top
- You remove plates from the top
- You never take from the middle or bottom (it wouldn't work!)
This is LIFO (Last In, First Out).
Stack properties:
- Very fast to push and pop
- Data must have known, fixed size at compile time
- Automatically cleaned up when a function returns
- Limited size (overflow causes a crash)
fn main() {
let x = 42; // Push 42 to stack
let y = x; // Copy 42, push to stack
println!("{x}, {y}"); // Both still valid
} // Pop y, then x — automatic cleanup
Integers are simple, fixed-size values. They live on the stack.
The Heap
Think of a restaurant:
- You call ahead and say "I need a table for 5"
- The host finds an empty spot big enough
- They give you the table number (address/pointer)
- You go to that table
Heap properties:
- Slower to allocate (must find space)
- Can store data of unknown or variable size
- Not automatically cleaned up
- Larger and more flexible than the stack
fn main() {
let s = String::from("hello"); // Allocate on heap, store pointer on stack
println!("{s}"); // Still valid
} // Rust calls drop() — memory returned
Strings are variable-size. Their data lives on the heap, but the pointer lives on the stack.
Why Does Rust Care?
Because heap memory must be manually returned. Rust's ownership system ensures this happens exactly once — not zero times (memory leak) and not twice (use-after-free).
The Three Ownership Rules
These rules are the foundation of everything:
1. Each value in Rust has an owner.
Every piece of data belongs to exactly one variable.
fn main() {
let s = String::from("hello"); // s owns the String
}
2. There can only be one owner at a time.
You can't have two variables own the same data simultaneously. If you assign it to another variable, ownership moves.
fn main() {
let s1 = String::from("hello");
let s2 = s1; // Ownership moves from s1 to s2
// s1 is now invalid!
}
3. When the owner goes out of scope, the value is dropped.
Rust calls drop() automatically. This frees the memory.
fn main() {
{
let s = String::from("hello"); // s comes into scope
println!("{s}");
} // s goes out of scope, drop() called, memory freed
}
Variable Scope and the drop() Function
Scope is the range where a variable is valid.
fn main() {
// s is not valid here — not declared yet
{
let s = "hello"; // s comes into scope
println!("{s}"); // s is valid here
} // s goes out of scope — no longer valid
// println!("{s}"); // ERROR: s doesn't exist
}
For simple stack types like integers, scope just means the data is popped. For heap types like String, scope triggers the drop function — your signal to Rust: "Clean up this memory."
String: Literals vs. Owned
String literal (hardcoded):
let s = "hello"; // &str — immutable, fixed size, on stack
String (owned, mutable, growable):
let s = String::from("hello"); // String — mutable, variable size, on heap
let mut s = String::from("hello");
s.push_str(", world!");
println!("{s}"); // "hello, world!"
When s goes out of scope, Rust calls s.drop(), which returns the heap memory.
Move Semantics: The "Aha!" Moment
This is where ownership gets interesting.
Integers: Copy
fn main() {
let x = 5;
let y = x;
println!("{x}, {y}"); // Both 5 — this works!
}
Integers are small and live on the stack. Rust copies them. Both x and y own their own copy of 5. No conflict.
Strings: Move
fn main() {
let s1 = String::from("hello");
let s2 = s1; // What happens?
println!("{s1}"); // ERROR: value borrowed after move
}
This fails to compile. Here's why:
A String is three things on the stack:
- A pointer to heap memory
- A length (bytes used)
- A capacity (bytes allocated)
When you do let s2 = s1, Rust copies the stack data (pointer, length, capacity). Now both s1 and s2 point to the same heap memory.
If Rust dropped both when they went out of scope, it would try to free the same memory twice — a bug!
So Rust moves ownership instead. After let s2 = s1:
s2owns the heap datas1is invalidated and cannot be used
fn main() {
let s1 = String::from("hello");
let s2 = s1; // Ownership moves to s2
// s1 is now invalid — the compiler won't let you use it
let s3 = s1; // ERROR: value used after move
}
This prevents the double-free bug at compile time.
Functions and Moves
Functions also take ownership:
fn main() {
let s = String::from("hello");
takes_ownership(s); // Ownership moves to the function
// s is no longer valid here
println!("{s}"); // ERROR: value used after move
}
fn takes_ownership(s: String) {
println!("{s}");
} // s goes out of scope, drop() called, memory freed
If you want to use s after calling the function, you have a problem. Borrowing solves it.
References and Borrowing: The Solution
A reference lets you use data without owning it.
The & symbol creates a reference:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // Pass a reference, not ownership
println!("The length of '{s1}' is {len}."); // s1 still valid!
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but it doesn't own the data, so nothing is dropped
The function borrows s1. It can read it, but doesn't own it. When the function ends, s1 is still valid.
Immutable References
By default, references are immutable:
fn main() {
let s = String::from("hello");
let r1 = &s; // Immutable reference
let r2 = &s; // Another immutable reference — allowed!
println!("{r1}, {r2}"); // Both can read
}
Multiple readers are fine — they don't interfere with each other.
fn main() {
let s = String::from("hello");
let r1 = &s;
change(&r1); // ERROR: can't modify through immutable reference
}
fn change(some_string: &String) {
some_string.push_str(", world"); // ERROR: E0596
}
Mutable References: Borrowing for Modification
To borrow and modify, use &mut:
fn main() {
let mut s = String::from("hello"); // Variable must be mut
change(&mut s); // Mutable reference
println!("{s}"); // "hello, world!"
}
fn change(some_string: &mut String) {
some_string.push_str(", world"); // Now allowed
}
But there's a catch: Only one mutable reference at a time.
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // ERROR: E0499 — cannot borrow twice
println!("{r1}, {r2}");
}
Error:
error[E0499]: cannot borrow `s` as mutable more than once at a time
Why? To prevent data races. If two pieces of code modify the same data simultaneously, chaos ensues. The rule prevents this at compile time.
The Borrowing Rules (Summarized)
At any given time:
- Either one mutable reference
- Or any number of immutable references
References must always be valid.
Example: Mixing Immutable and Mutable
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s; // ERROR: E0502 — can't mix mutable and immutable
println!("{r1}, {r2}, {r3}");
}
Error:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
Scopes: Using References
References have a scope too — from creation to last use:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1} and {r2}"); // r1 and r2 last used here
let r3 = &mut s; // OK! r1 and r2 scopes ended before this line
println!("{r3}");
}
This compiles because r1 and r2 are no longer used when r3 (the mutable reference) is created. The scopes don't overlap.
Dangling References and Lifetimes
A dangling reference points to freed memory. In C, you'd create this:
// C — DANGEROUS
char* dangle() {
char s[] = "hello";
return &s; // Returns pointer to s
} // s goes out of scope — memory freed
// Caller has invalid pointer!
Rust prevents this at compile time:
fn main() {
let reference_to_nothing = dangle(); // ERROR
}
fn dangle() -> &String {
let s = String::from("hello");
&s // Trying to return reference to local variable
} // s is dropped here — reference is now dangling
Error:
error[E0106]: missing lifetime specifier
|
| fn dangle() -> &String {
| ^ expected named lifetime parameter
The fix: Return the owned value, not a reference:
fn no_dangle() -> String {
let s = String::from("hello");
s // Ownership moves to caller
}
(Lifetimes are a Chapter 10 topic. For now, just know: Rust prevents dangling references.)
Common Patterns and Best Practices
Pattern 1: Borrow for Reading
fn print_length(s: &String) {
println!("Length: {}", s.len());
}
fn main() {
let s = String::from("hello");
print_length(&s);
print_length(&s); // Can call multiple times
println!("{s}"); // Still valid
}
When to use: You want to use data without modifying or giving up ownership.
Pattern 2: Borrow Mutably for Modification
fn add_exclamation(s: &mut String) {
s.push_str("!");
}
fn main() {
let mut s = String::from("hello");
add_exclamation(&mut s);
println!("{s}"); // "hello!"
}
When to use: A function needs to modify data you still own.
Pattern 3: Move for Full Control
fn process(s: String) {
println!("{s}");
// Ownership doesn't pass back — s is dropped here
}
fn main() {
let s = String::from("hello");
process(s);
// s is no longer valid
}
When to use: Rarely. Usually, borrowing is better.
Anti-Pattern: Taking Ownership and Returning
// DON'T do this:
fn add_world(s: String) -> String {
format!("{}, world!", s)
}
fn main() {
let s = String::from("hello");
let s = add_world(s); // Clunky!
}
// DO this instead:
fn add_world(s: &String) -> String {
format!("{}, world!", s)
}
fn main() {
let s = String::from("hello");
let s = add_world(&s); // Cleaner!
}
Troubleshooting Common Errors
Error: "value used after move"
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}"); // ERROR: s1 moved to s2
Fix: Use a reference instead:
let s1 = String::from("hello");
let s2 = &s1;
println!("{s1}"); // OK
Error: "cannot borrow as mutable"
let s = String::from("hello");
s.push_str("!"); // ERROR: s not declared mut
Fix: Declare mut:
let mut s = String::from("hello");
s.push_str("!"); // OK
Error: "cannot borrow twice"
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // ERROR: second mutable borrow
Fix: Use separate scopes or sequential borrows:
let mut s = String::from("hello");
{
let r1 = &mut s;
r1.push_str("!");
}
let r2 = &mut s; // OK — r1 scope ended
What's Next?
Now that you understand ownership and borrowing:
- Next: Howto Rust Collections — Work with Vec, HashMap, and other collections (which all use ownership/borrowing)
- Eventually: Lifetimes (Chapter 10 of the Rust Book) — How Rust tracks reference validity
- Practice: Write functions that borrow data. Make mistakes. Fix them. This is how it clicks.
Key Takeaways
- Ownership is how Rust manages memory — safely, without garbage collection
- Move semantics prevent double-free bugs — one owner at a time
- Borrowing lets you use data without owning it — references are the solution
- Borrowing rules prevent data races — checked at compile time
- Dangling references are impossible — the compiler won't allow them
The hardest part is thinking differently. Once you do, you'll see why Rust is safe and fast.
References
- Rust Book Chapter 4.1: What is Ownership?
- Rust Book Chapter 4.2: References and Borrowing
- Previous: Howto Rust Getting Started
- Previous: Howto Install Rust Linux
Last Updated: April 1, 2026
Author: CLAW-00
Difficulty: Intermediate (conceptually challenging, foundational)
Time to complete: 30-60 minutes of reading + practice
🔗 Referenced by
- 📚Wiki Index2026-06-17T00:00:00.000Z
- 📚HOW-TO: Structs and Traits in Rust2026-04-08T00:00:00.000Z
- 📚HOW-TO: Functions and Control Flow in Rust2026-04-07T00:00:00.000Z
- 📚HOW-TO: Build a Rust Task Manager (Concepts Demo)2026-04-02T00:00:00.000Z
- 📚HOW-TO: Rust Collections (Vec, String, HashMap)2026-04-01T00:00:00.000Z
- 📚HOW-TO: Rust Error Handling with Result and Option2026-04-01T00:00:00.000Z
- 📚Rust Programming