Ownership in Rust Explained With a Mental Model That Holds
Ownership is three rules the compiler checks for every value. This post builds a mental model from them and uses it to predict what compiles before you run the compiler.

Ownership is the idea that makes Rust different, and it is usually taught as a list of rules to memorize. That works until you meet a situation the rules do not obviously cover. A mental model works better: every value in a Rust program has exactly one owner, the owner is responsible for cleaning the value up, and everything else in the language is a way of letting other code use a value without taking that responsibility. This post builds that model from one runnable program that prints when each value is cleaned up, so you can watch the owner do its job.
The program ran on Rust 1.99 in edition 2024. Every line of output below is what it printed, and the errors are the compiler's own.
The model in three sentences
A value has one owner, a variable or a field that is responsible for it. When the owner goes out of scope, the value is dropped, meaning its destructor runs and its memory is freed. Passing a value to a function by value, or assigning it to another variable, moves the responsibility to the new owner, and the old name can no longer be used.
Everything else, references, borrowing, lifetimes, Clone, Rc, follows from those sentences. References let code look at a value without owning it. Lifetimes are the compiler's proof that a reference does not outlive its owner. Clone makes a second value with its own owner. Rc makes ownership shared on purpose, with a count of owners. If you can state the owner of every value in a function, you can predict what the compiler will accept.
Watching ownership happen
To see owners and drops, the program defines a type that prints a message when it is dropped, and moves it through five situations:
struct Resource(&'static str);
impl Drop for Resource {
fn drop(&mut self) {
println!(" dropped {}", self.0);
}
}
fn take(r: Resource) {
println!(" take() has {}", r.0);
} // r is dropped here: take() was the owner
fn borrow(r: &Resource) {
println!(" borrow() sees {}", r.0);
}
fn make(name: &'static str) -> Resource {
println!(" making {name}");
Resource(name)
}
fn main() {
println!("1. scope ends drop");
{
let _a = make("a");
println!(" end of block");
}
println!("2. a move transfers ownership");
let b = make("b");
take(b);
println!(" back in main, b is gone");
println!("3. a borrow does not");
let c = make("c");
borrow(&c);
borrow(&c);
println!(" c still usable");
println!("4. reassignment drops the old value");
let mut d = make("d1");
d = make("d2");
println!(" d is now {}", d.0);
println!("5. locals drop in reverse order");
let _e = make("e");
let _f = make("f");
println!(" end of main");
}1. scope ends drop
making a
end of block
dropped a
2. a move transfers ownership
making b
take() has b
dropped b
back in main, b is gone
3. a borrow does not
making c
borrow() sees c
borrow() sees c
c still usable
4. reassignment drops the old value
making d1
making d2
dropped d1
d is now d2
5. locals drop in reverse order
making e
making f
end of main
dropped f
dropped e
dropped d2
dropped cRead the output against the code, one situation at a time.
Situation one: a is created inside a block and dropped at the closing brace, after "end of block" prints. The owner's scope ended, so the value was cleaned up at that exact line. There is no garbage collector deciding when; the drop point is visible in the source, and the output confirms it. This is the property that makes Rust's destructors reliable for files, locks and network connections: a File closes when its owner goes out of scope, every time, even if the function returns early with ?.
Situation two: take(b) moves b into the function. Inside, take is the owner, and dropped b prints when the function ends, before control returns to main. By the time "back in main" prints, b is already gone, and writing b after the call would be a compile error. That is the model working: after a move, the old name owns nothing.
Situation three: borrow(&c) passes a reference. The function can look at c but is not the owner, so nothing is dropped when it returns, and c is still usable. The two calls print twice and c survives both. Borrowing is how you let a function use a value without giving it away.
Situation four: assigning to d a second time drops the first value immediately, which is why dropped d1 appears right after making d2 and before the next print. A variable can only own one value, so replacing the value ends the old one.
Situation five: the end of main drops its remaining locals in reverse order of declaration: f, then e, then d2, then c. Reverse order matters because later variables may borrow from earlier ones, so the earlier ones must outlive them. The value b is not in this list. It was moved into take and dropped there.
What the compiler enforces
The compiler checks the model at compile time, and each rule corresponds to an error you will meet. Using a value after moving it is E0382:
error[E0382]: borrow of moved value: `name`
--> e0382a.rs:8:29
|
6 | let name = String::from("ferris");
| ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
7 | let n = consume(name);
| ---- value moved here
8 | println!("{n} bytes in {name}");
| ^^^^ value borrowed here after moveThe three marked lines are the declaration, the move and the late use. If you can say who owned the string at each point, the error is not mysterious: main owned it on line 6, consume owned it from line 7, and main tried to use it on line 8 after giving it away.
You also cannot take ownership of something you only borrowed. This is E0507, "cannot move out of ... behind a shared reference":
error[E0507]: cannot move out of `r.name` which is behind a shared reference
--> e0507.rs:8:17
|
8 | let taken = r.name;
| ^^^^^^ move occurs because `r.name` has type `String`, which does not implement the `Copy` trait
|
help: consider borrowing here
|
8 | let taken = &r.name;
help: consider cloning the value if the performance cost is acceptable
|
8 | let taken = r.name.clone();A reference to a Holder means someone else owns it. Taking its name out would leave the real owner with a hole. The compiler offers the two sound alternatives: borrow the field as well, or clone it so that the new owner has its own copy.
Three questions that resolve most problems
When ownership code does not compile or you are unsure how to write it, ask these in order.
Who should own this value? Give the data one clear owner, usually the part of the program that decides how long it lives. A configuration object is owned by main, a request body by the handler, a cache by the service.
Does this function need to own it, or only look at it? A function that only reads takes &T or &str. A function that stores the value, sends it to another thread or returns it needs T. Writing the right parameter type is the biggest single skill in everyday Rust.
If several parts need it at once, is that sharing or copying? Copying means .clone() and each part has its own value. Sharing means Rc<T> on one thread or Arc<T> across threads, each part holds a counted handle to one value, and the value is dropped when the last handle goes. The choice is cost and semantics: cloning a large vector copies it, cloning an Arc increments a number.
Where the model has edges
Some types do not move: integers, floats, bool, char and references implement Copy, so assigning them duplicates the bits and both names stay valid. That is why let b = a; works for a number and moves a string. Copy versus Clone covers exactly where the line sits.
Ownership is also per value, not per variable name. A struct that owns a String field owns it through the struct: when the struct is dropped, the field is dropped. A Vec<String> owns all of its strings. A Box<T> owns a value on the heap. The model composes: dropping an owner drops everything it owns, recursively, which is why a Rust program needs no manual cleanup of nested data.
References add one rule on top. You can have either any number of shared references or exactly one mutable reference to a value at a time, and a reference must not outlive its owner. The sibling errors are E0502, E0499 and E0597, and they are all this one rule seen from different angles.
Why Rust does it this way
The alternatives each have a cost. A garbage collector frees memory at times you do not control and pays for tracking at run time. Manual malloc and free give control and let you free twice, free too early or forget. Reference counting everywhere is simple and costs an atomic increment per copy and cannot handle cycles. Ownership gives deterministic cleanup with no runtime cost, at the price of stating who owns what, which is the cost this whole post is about. In return, the same rules that free memory also rule out data races: a value that can be changed by only one owner at a time cannot be changed by two threads at once.
When the model does not fit, as in a graph where nodes refer to each other, the usual answer is to store indices or IDs into a Vec instead of references, or to reach for Rc<RefCell<T>> with eyes open about the run-time checks it introduces. For the day-to-day patterns that follow from the model, see Copy versus Clone and the error E0382 in detail.


