A C program that frees a buffer and keeps reading from it still compiles. Then it corrupts memory, sometimes immediately, sometimes three requests later in a part of the code that never touched that buffer. Rust rejects that program at cargo build, and there’s no garbage collector involved. The compiler tracks who owns each value and who is allowed to look at it. That tracking is ownership and borrowing.
What are Rust’s ownership rules?
Rust’s ownership model comes down to three rules:
- Every value has exactly one owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped.
“Dropped” means its destructor runs and its memory is freed immediately, not whenever a garbage collector gets around to it. That’s the deal Rust makes: no runtime deciding when to reclaim memory, in exchange for the compiler enforcing these rules on every line you write.
Rule 2 is the one that trips people up first. Assign a String to a second variable and the first one stops working:
| |
| |
let s2 = s1 doesn’t copy the string’s bytes. String holds a pointer to a heap buffer, a length, and a capacity. Copying that struct bit-for-bit would leave two owners pointing at the same buffer, and when both went out of scope, the buffer would be freed twice. Rust avoids that by making the assignment a move: s2 takes over ownership, and s1 is no longer valid. Passing a String into a function does the same thing. The function takes ownership, and the variable you passed is gone back in the caller.
Why don’t integers move in Rust?
Integers don’t have this problem:
| |
This compiles. i32 implements the Copy trait, so assignment duplicates the value instead of moving it. There’s nothing on the heap to double-free, only four bytes on the stack. Every integer type, bool, every floating-point type, char, and tuples made entirely of Copy types ((i32, bool), not (i32, String)) behave the same way. A type can implement Copy only if none of its parts implement Drop, which is Rust’s way of saying: if cleanup matters, duplication has to be explicit.
When you want a second, independent copy of a String or a Vec, say so with .clone(). It’s an extra heap allocation, which is exactly why Rust doesn’t do it silently on every assignment.
How does borrowing work in Rust?
If every function that needed to read a value had to take ownership of it, you’d be handing values back out of every function in a return tuple just to keep using them. Borrowing removes that: take a reference with &, use the value, and ownership never changes hands.
| |
s1 is still valid after the call, because calculate_length only borrowed it. The parameter type &String is a reference: it points at s1’s data without owning it, so there’s nothing to drop when the borrow ends.
Two rules keep this safe:
- At any given time, you can have either one mutable reference to a value, or any number of immutable references, never both.
- A reference can never outlive the data it points to.
Break the first one and the compiler stops you:
| |
| |
Two mutable references to the same value at once get rejected too (error[E0499]: cannot borrow `s` as mutable more than once at a time). Neither error is arbitrary. An immutable reference is a promise to the rest of the program that the value won’t change underneath it, and a mutable reference needs exclusivity to keep that promise. The classic thread-safety bug in C or Go, where two pieces of code write to the same memory while a third reads it, is exactly what these rules make impossible to compile.
That doesn’t mean Rust catches every race condition you could write. Safe Rust can still deadlock on a mutex or get the ordering of operations wrong, and unsafe blocks can bypass the checks entirely. What the borrow checker eliminates is one specific, very common shape of the bug: aliased mutable memory, caught before the program runs.
What do lifetime annotations like ‘a mean?
Every reference has a lifetime: the scope over which it’s valid. Usually the compiler works this out on its own. It needs your help when a function returns a reference and more than one input reference is in play, because it has no way to know which input the output is tied to.
| |
'a is a lifetime parameter, read “tick-a”. It’s a name, not a duration. Writing &'a str on both parameters and the return type tells the compiler: the reference I hand back lives at most as long as the shorter-lived of x and y. The annotation doesn’t change how long anything lives. It describes a relationship the compiler already has to verify, so it can reject a call where the returned reference would outlive one of its inputs.
You don’t write 'a everywhere, because Rust elides it in the common cases. A function with exactly one reference parameter gets that lifetime assigned to its return type automatically, and a method taking &self assigns self’s lifetime to the return type. That’s why fn first_word(s: &str) -> &str compiles with no annotation at all: one input, one output, nothing ambiguous. longest takes two independent reference parameters and no self, so elision has nothing to latch onto, and you write the lifetime by hand.
What to do when the borrow checker fights your design
The borrow checker’s complaints feel adversarial for the first few weeks. And sometimes the straightforward version of a data structure doesn’t fit one-owner-at-a-time at all: a tree with parent pointers, or a cache that two subsystems both need to mutate. The pragmatic ways out:
.clone()the value instead of fighting a borrow. It costs an allocation, and it’s a one-line fix you can optimize later once the rest of the code works.Rc<T>(orArc<T>across threads) for shared ownership, when more than one part of the program legitimately needs to keep a value alive.RefCell<T>(orMutex<T>across threads) when you need to mutate something through a shared reference. This moves the borrow check from compile time to runtime, trading a compiler error for a panic if you get it wrong.
None of these are workarounds you should feel bad about reaching for. They also hint at why this model isn’t right for every project. A one-off script that reads a file and prints a report gains nothing from tracking ownership down to the byte, and a garbage-collected language with goroutines will get you to a working concurrent program faster if compile-time memory safety isn’t the requirement. Rust pays off when a use-after-free or a data race in production is the failure you can’t afford, and you’d rather pay for it upfront in compiler errors.
The next time rustc rejects your code with E0382 or E0502, read the error before reaching for .clone() out of reflex. It names the two borrows in conflict and where each one starts, and fixing the scope issue it points at usually takes less code than papering over it. Those messages are the fastest way to learn the model; they’re more precise than most explanations of it, this one included.