Rust Ownership and Borrowing Explained
A practical introduction to Rust's ownership and borrowing rules: moves, references, mutability, and the mental model that makes the borrow checker stop fighting you.
What you'll learn
- ✓What ownership means in Rust
- ✓The three borrowing rules and why they exist
- ✓When values are moved vs copied
- ✓How to read borrow checker errors
- ✓Patterns that keep the borrow checker happy
Prerequisites
- •Basic familiarity with the language
Rust does not have a garbage collector. It does not require you to call free. It still guarantees memory safety. The trick is ownership: a small set of rules the compiler enforces at compile time. Learn them and most Rust borrow checker fights disappear.
Ownership: one owner at a time
Every value in Rust has exactly one owning variable. When the owner goes out of scope, the value is dropped (memory freed, file closed, etc.).
fn main() {
let s = String::from("hello"); // s owns the string
// ... use s ...
} // s goes out of scope, the string is dropped here
Assigning a value to another variable transfers ownership. The original variable can no longer be used.
let a = String::from("hello");
let b = a; // ownership moves from a to b
println!("{}", a); // ERROR: value borrowed after move
This is a move, not a copy. The heap allocation behind the string is not duplicated; only the pointer/length/capacity triple is bitwise copied, and the source is invalidated.
For types that are cheap to duplicate (integers, bools, small fixed-size things), Rust implements the Copy trait and assignment copies instead of moves. That is why this works:
let x = 5;
let y = x; // copy, not move
println!("{}", x); // fine
Borrowing: references without transferring ownership
If you want to lend a value to another function without giving up ownership, you pass a reference.
fn length(s: &String) -> usize {
s.len()
}
let s = String::from("hello");
let n = length(&s);
println!("{} has {} chars", s, n); // s is still valid
&s is a shared reference (immutable borrow). The function reads from the string but does not own it. When the function returns, the borrow ends and the owner can use the value again.
For mutation, use &mut.
fn push_world(s: &mut String) {
s.push_str(", world");
}
let mut s = String::from("hello");
push_world(&mut s);
The borrowing rules
The compiler enforces two rules at all times:
- At any given time, you can have either one mutable reference or any number of shared references.
- References must always be valid (no dangling references).
That is it. Every borrow checker error reduces to one of those two.
Allowed: Not allowed:
&T &T &T (many readers) &T &mut T (read while write)
&mut T (one writer alone) &mut T &mut T (two writers) The rule is sometimes called aliasing XOR mutability. It is the foundation of Rust’s data race freedom.
The mental model
Think of each value as having a single owner and a budget of borrows. While shared borrows are out, no one can mutate. While a mutable borrow is out, no one else can touch the value at all. The compiler tracks these intervals at scope granularity (with non-lexical lifetimes, ending borrows at the last use).
let mut v = vec![1, 2, 3];
let r = &v[0]; // shared borrow starts
println!("{}", r); // shared borrow ends here (last use)
v.push(4); // OK: no live borrow
The earlier rule about “one or the other” applies at the granularity of when references are actually used, not when they are introduced.
Hands-on example
Computing a sum and the first element together, the wrong way and the right way.
// Wrong: tries to hold a shared borrow across a mutation
fn broken(v: &mut Vec<i32>) -> (i32, i32) {
let first = &v[0];
let sum: i32 = v.iter().sum(); // shared borrow here
v.push(0); // ERROR: mutable borrow while shared live
(*first, sum)
}
// Right: take what you need, then mutate
fn ok(v: &mut Vec<i32>) -> (i32, i32) {
let first = v[0]; // copies an i32
let sum: i32 = v.iter().sum();
v.push(0); // fine: no live borrows
(first, sum)
}
For Copy types, copying out solves most borrowing puzzles. For larger values, structure your code so that reads and writes do not overlap, or clone explicitly when you need to.
Common pitfalls
Returning references from functions that create the value. The owner goes out of scope at the end of the function; the reference would dangle.
fn bad() -> &String {
let s = String::from("hi");
&s // ERROR: returns reference to local
}
Return the owned value instead, or take a reference parameter and return a reference into it.
Iterator invalidation, Rust style. Modifying a vector while iterating over it is a borrow violation.
let mut v = vec![1, 2, 3];
for x in &v {
v.push(*x); // ERROR: cannot borrow as mutable
}
Trying to express “everyone can have a pointer to this object.” That is what Rc and RefCell are for, and you usually do not need them.
Practical tips
Read borrow checker errors in full. They say which line introduced the borrow, which line conflicts with it, and where the borrow would end. Once you start tracing those three points, errors become routine.
Prefer ownership over borrowing when in doubt. Cloning a String or moving a struct is rarely the performance problem you fear, and it sidesteps a class of borrow puzzles.
Split structs along borrowing boundaries. If you keep finding yourself wanting to borrow two unrelated fields mutably, split them into two structs or use the split borrowing patterns the compiler accepts.
Wrap-up
Ownership and borrowing are not arbitrary rules; they are a compile-time encoding of who can read and who can write. Once you internalize that there is exactly one owner and that borrows enforce aliasing XOR mutability, the language stops feeling adversarial. Most fights with the borrow checker are about expressing the wrong design, and the compiler is doing you the favor of telling you so before runtime does.
Related articles
- Rust Rust Smart Pointers: Box, Rc, and Arc
Understand when to reach for Box, Rc, and Arc in Rust, how each interacts with the borrow checker, and the cost of shared ownership across threads.
- Rust Rust Ownership: The Idea That Makes Rust Different
A friendly tour of Rust's ownership system — the three rules, move semantics, Copy vs Clone, borrowing with & and &mut, the borrow checker, and the beginner errors you'll see first.
- Rust Rust Actix vs Axum Comparison
A side-by-side comparison of Actix Web and Axum, covering architecture, ergonomics, performance, ecosystem, and how to pick the right one for your project.
- Rust Rust async/await and Futures Explained
How Rust's async/await desugars into a state machine, what a Future actually is, and the runtime model that makes it efficient on real workloads.