References and Borrowing in Rust
At the end of Ownership we hit an annoying wall. We wanted a function to simply look at a String and tell us how long it was — but because passing a value moves it, we had to hand ownership in and then ship it back out again just to keep using our own variable:
let (pet_back, letters) = count_letters(pet); // give it away, get it back...That’s absurd. You shouldn’t have to give away your book so somebody can count its pages.
Rust’s answer is the reference — a way to let code use a value without taking it. Creating a reference is called borrowing, and this chapter is about how it works, why it has rules, and how to read the compiler errors it produces.
A reference lets you use a value that somebody else still owns.
This chapter assumes you’re comfortable with moves, clone(), and the Copy trait. If any of that is hazy, take five minutes with Ownership first — everything here builds directly on it.
The Mental Model: A Library
The last chapter compared a value to a physical book: only one person holds it, and handing it over means it’s no longer yours.
Borrowing is what a library adds to that picture:
| In a library | In Rust | Written as |
|---|---|---|
| You own the book on your shelf | The owning variable | let menu = String::from(...) |
| Friends read it at the table with you — as many as want | Shared reference | &menu |
| One person checks it out to annotate it | Mutable reference | &mut menu |
| Everyone returns it; it’s still yours | The borrow ends, owner unaffected | — |
| A catalog card for a book that was destroyed | Dangling reference | impossible in Rust |
Two rules that any real library would also enforce:
- Any number of people can read the same copy at the same time.
- Only one person can write in it — and while they do, nobody else may even read it (they’d see half-finished scribbles).
Hold onto that. Those two sentences are the borrow checker.
Your First Reference
Here is the length problem solved properly. Note the & in two places:
fn main() {
let nickname = String::from("shelly");
let size = letter_count(&nickname);
println!("'{nickname}' has {size} letters.");
}
fn letter_count(text: &String) -> usize {
text.len()
}cargo runOutput:
'shelly' has 6 letters.No tuple. No handing the value back. nickname is still perfectly usable on the last line — in fact we print it after the function call.
Look at where the two & symbols go:
&nicknameat the call site creates a reference tonicknameinstead of moving it.&Stringin the signature says “this parameter is a reference to aString, not aString.”
They have to match. A function taking &String must be given &something.
What This Looks Like in Memory
This is the diagram worth memorizing:
A reference points at the owner, which points at the data. Nothing on the heap is copied or created.
Notice there are two hops now. text doesn’t point straight at the letters — it points at nickname, and nickname points at the letters. That extra hop is the whole trick: because text only borrows, nickname never stops being the owner, and nothing about the heap allocation changes.
A reference is genuinely tiny — it’s one address, the same size as an integer. Passing &nickname is about as cheap as an operation gets.
References are not the same as clone(). .clone() duplicates the heap data and gives you a second independent value that costs real work to create. & copies nothing at all — it just writes down an address. When you only need to read something, a reference is almost always the right answer.
Borrowing: The Word for What Just Happened
Creating a reference is called borrowing, exactly like borrowing something in real life: you get temporary use of it, you don’t own it, and you give it back.
The most important consequence: a reference never frees anything. When letter_count ends, text goes out of scope — but since text never owned the String, no drop happens and the heap data is untouched.
You can prove it by borrowing over and over:
fn main() {
let shell = String::from("spiral");
inspect(&shell);
inspect(&shell);
inspect(&shell);
println!("still alive in main: {shell}");
}
fn inspect(item: &String) {
println!("inspecting {item} ({} bytes)", item.len());
}Output:
inspecting spiral (6 bytes)
inspecting spiral (6 bytes)
inspecting spiral (6 bytes)
still alive in main: spiralThree borrows, zero moves, one allocation from start to finish. Compare that to the ownership chapter, where a single call would have consumed shell forever.
The same function call, with and without &. One line of difference, completely different outcome for the caller.
Many Readers Are Always Fine
Nothing limits you to one shared reference. Make as many as you like:
fn main() {
let habitat = String::from("coral reef");
let peek_one = &habitat;
let peek_two = &habitat;
let peek_three = &habitat;
println!("{peek_one} / {peek_two} / {peek_three}");
println!("the owner still works: {habitat}");
}Output:
coral reef / coral reef / coral reef
the owner still works: coral reefFour ways to reach the same string — three borrowers plus the owner — and they all work. This is safe for one reason: none of them can change anything. If nobody can write, everybody reading sees the same thing, and there’s nothing to go wrong.
Dereferencing with *
& builds a reference. Its opposite, *, follows a reference back to the value. That’s called dereferencing:
fn main() {
let claws = 2;
let pointer_to_claws = &claws;
println!("the reference itself: {pointer_to_claws}");
println!("dereferenced with *: {}", *pointer_to_claws);
println!("do they match? {}", *pointer_to_claws == claws);
}Output:
the reference itself: 2
dereferenced with *: 2
do they match? trueYou’ll notice we barely ever wrote * in the earlier examples — we called text.len() directly, not (*text).len(). That’s because Rust has automatic dereferencing: when you use . on a reference, the compiler inserts the * for you. Same for println!, and for comparisons like ==.
In everyday Rust you write * far less than you’d expect. It shows up mostly when you want to replace the value behind a &mut, or when working with the smart pointer types in a later chapter. If you’re not sure whether you need it, try without it first — the compiler will tell you.
References Are Read-Only by Default
Here’s the first rule that bites people. In Rust, &T gives you read access, and only read access. Watch this fail:
fn main() {
let tank_label = String::from("tank a");
add_stars(&tank_label);
}
fn add_stars(label: &String) {
label.push_str(" ***");
}error[E0596]: cannot borrow `*label` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
8 | label.push_str(" ***");
| ^^^^^ `label` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
7 | fn add_stars(label: &mut String) {
| +++
For more information about this error, try `rustc --explain E0596`.This mirrors something you already know from Variables and Mutability: variables are immutable unless you write mut. References work the same way — a plain & is a read-only loan, and the compiler even tells you the exact fix.
Mutable References: &mut
To borrow something and be allowed to change it, you need &mut. Three things have to line up:
fn main() {
let mut tank_label = String::from("tank a");
add_stars(&mut tank_label);
println!("{tank_label}");
}
fn add_stars(label: &mut String) {
label.push_str(" ***");
}Output:
tank a ***The three changes, and all three are required:
- The owner must be declared
mut— you can’t hand out write access to something that isn’t writable. - The call site passes
&mut tank_labelinstead of&tank_label. - The parameter’s type is
&mut Stringinstead of&String.
And notice the payoff: main still owns tank_label after the call, but the function was able to modify it in place. No move, no clone, no return value.
Forgetting mut on the Owner
Miss step 1 and you get a different flavour of the same error. It’s a common one:
fn main() {
let logbook = String::from("day 1");
let pen = &mut logbook;
pen.push_str(" - calm");
}error[E0596]: cannot borrow `logbook` as mutable, as it is not declared as mutable
--> src/main.rs:4:15
|
4 | let pen = &mut logbook;
| ^^^^^^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut logbook = String::from("day 1");
| +++You cannot lend out more power than you have. If logbook itself can’t be changed, no reference to it can change it either.
The One Big Restriction
Now for the rule that makes people argue with the compiler. You may only have one mutable reference to a value at a time.
fn main() {
let mut logbook = String::from("day 1");
let first_pen = &mut logbook;
let second_pen = &mut logbook;
println!("{first_pen}, {second_pen}");
}error[E0499]: cannot borrow `logbook` as mutable more than once at a time
--> src/main.rs:5:22
|
4 | let first_pen = &mut logbook;
| ------------ first mutable borrow occurs here
5 | let second_pen = &mut logbook;
| ^^^^^^^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{first_pen}, {second_pen}");
| ----------- first borrow later used here
For more information about this error, try `rustc --explain E0499`.Read the error closely, because it tells you exactly how the compiler thinks. It doesn’t just say “two mutable borrows exist.” It points at line 7 and says “first borrow later used here” — the first borrow is a problem only because you use it again afterwards. That detail becomes very useful in a moment.
Why? Because This Is How Data Races Happen
This restriction isn’t the compiler being fussy. It eliminates an entire category of bug: the data race.
A data race needs three ingredients at once:
Take away any one ingredient and a data race is impossible. Rust takes away the first whenever the second is present.
Data races are notoriously horrible to debug: the program works ninety-nine times and corrupts itself the hundredth, and the failure often disappears when you add a print statement. In most languages you find them at runtime, in production, at 3am. In Rust you find them at compile time, every time, and you can’t ship the bug even if you want to.
The Rule, In One Picture
At any given moment a value is in one of these two states — never a mixture of both.
Formally:
At any given time, you may have either:
- any number of immutable references (
&T), or - exactly one mutable reference (
&mut T).
Never both at the same time.
Think of it as many readers or one writer. It’s the same rule that databases and file locks use, enforced by a compiler instead of at runtime.
Working With the Borrow Checker
The rule sounds restrictive. In practice it’s much softer than it looks, because a borrow doesn’t last as long as you probably assume. Here are the three techniques that resolve nearly every borrow error you’ll meet.
1. A Borrow Ends at Its Last Use
This is the single most useful thing to know in this whole chapter, and it’s why the E0499 error above pointed at the println! line.
A reference’s lifetime does not run to the end of the enclosing block. It starts where the reference is created and ends at the last place it’s actually used. The compiler feature responsible is called non-lexical lifetimes (NLL).
So two mutable borrows in the same function are completely fine, as long as their uses don’t overlap:
fn main() {
let mut logbook = String::from("day 1");
let first_pen = &mut logbook;
first_pen.push_str(" - calm");
// first_pen is never used again → its borrow ends right here
let second_pen = &mut logbook;
second_pen.push_str(" | day 2");
println!("{logbook}");
}Output:
day 1 - calm | day 2Two &mut borrows of the same variable, no error. The rule was never “one mutable reference per scope” — it’s “one mutable reference at a time.”
Same two borrows, different ordering. Move the last use and the error disappears.
The practical fix for most borrow errors is therefore: finish using the old reference before creating the new one.
2. End a Borrow Early With a Block
If you can’t reorder the code, you can force a borrow to end by wrapping it in braces. When the block closes, the reference inside it is gone for good:
fn main() {
let mut logbook = String::from("day 1");
{
let first_pen = &mut logbook;
first_pen.push_str(" - calm");
} // first_pen goes out of scope here — the borrow is definitely over
let second_pen = &mut logbook;
second_pen.push_str(" | day 2");
println!("{logbook}");
}Output:
day 1 - calm | day 2With NLL you rarely need this any more, but it’s still handy when you want to be explicit, or when a borrow is held by something whose last use isn’t obvious.
3. Don’t Mix Readers and a Writer
The one combination that’s never allowed is a mutable reference alongside a shared one:
fn main() {
let mut menu = String::from("kelp");
let reader_a = &menu;
let reader_b = &menu;
let writer = &mut menu;
println!("{reader_a}, {reader_b}, and {writer}");
}error[E0502]: cannot borrow `menu` as mutable because it is also borrowed as immutable
--> src/main.rs:6:18
|
4 | let reader_a = &menu;
| ----- immutable borrow occurs here
5 | let reader_b = &menu;
6 | let writer = &mut menu;
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("{reader_a}, {reader_b}, and {writer}");
| ---------- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.E0502 is the error you’ll meet most often in real code. The reasoning is the readers’ point of view: reader_a and reader_b were promised a value that doesn’t change underneath them. Handing out writer would break that promise.
Notice reader_a and reader_b coexisting happily on lines 4 and 5 — two shared borrows are never a problem. The trouble starts only when a writer joins them.
And because of rule 1, the fix is usually just reordering:
fn main() {
let mut menu = String::from("kelp");
let reader_a = &menu;
let reader_b = &menu;
println!("{reader_a} and {reader_b}");
// both readers are done → their borrows end here
let writer = &mut menu;
writer.push_str(" + shrimp");
println!("{writer}");
}Output:
kelp and kelp
kelp + shrimpIdentical borrows, just used in an order where they never overlap. The compiler is happy.
The Decision the Compiler Makes
Every time you write & or &mut, the borrow checker runs this check:
Remember that “currently alive” means up to its last use — which is why reordering fixes so much.
Why This Rule Earns Its Keep
At this point the rule can still feel like bureaucracy. Here’s the example that changes minds — and it has nothing to do with threads:
fn main() {
let mut crabs = vec![String::from("shelly")];
let oldest = &crabs[0];
crabs.push(String::from("pinchy"));
println!("the oldest crab is {oldest}");
}error[E0502]: cannot borrow `crabs` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let oldest = &crabs[0];
| ----- immutable borrow occurs here
5 |
6 | crabs.push(String::from("pinchy"));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("the oldest crab is {oldest}");
| -------- immutable borrow later used hereThat looks innocent, doesn’t it? Take a reference to the first element, add another element, print the first one.
But a vector stores its elements in one contiguous heap block. When you push past its capacity, it must reallocate: allocate a bigger block, copy everything over, and free the old one. The instant that happens, oldest points into freed memory. Printing it would read whatever happened to land there.
In C++ this exact pattern compiles cleanly and is one of the most common sources of security vulnerabilities — it’s called iterator invalidation, and it’s a use-after-free. In Rust it is a compile error you cannot ignore.
This is the real reason for the borrow rules. It isn’t about threads — it’s about the fact that modifying a collection can move its contents in memory, invalidating every reference into it. “One writer, no readers” is precisely the condition that makes that safe.
Dangling References
Last problem, and Rust’s answer is characteristically absolute. In a language with raw pointers, you can return a pointer to a local variable — and get a dangling pointer, aimed at memory that’s already been freed.
Try it in Rust:
fn main() {
let lost = make_tag();
println!("{lost}");
}
fn make_tag() -> &String {
let temp = String::from("pinchy");
&temp
}error[E0106]: missing lifetime specifier
--> src/main.rs:7:18
|
7 | fn make_tag() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
7 | fn make_tag() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
7 - fn make_tag() -> &String {
7 + fn make_tag() -> String {
|
For more information about this error, try `rustc --explain E0106`.The key line is the help: “this function’s return type contains a borrowed value, but there is no value for it to be borrowed from.” temp is created inside make_tag and dropped the moment the function ends. A reference to it would point at nothing.
Stage 2 is what would happen in a language that allowed this. Rust never lets you get there.
The phrase “missing lifetime specifier” hints at a bigger feature — lifetimes — which give you a way to tell the compiler how long a returned reference stays valid. There’s a whole chapter on them later. For now, just read E0106 as: “you’re trying to return a reference to something that’s about to be destroyed.”
The Fix: Return Ownership Instead
If there’s no value left to borrow from, don’t return a borrow — return the value itself:
fn main() {
let tag = make_tag();
println!("{tag}");
}
fn make_tag() -> String {
let temp = String::from("pinchy");
temp
}Output:
pinchyOne character deleted. Now ownership moves out to the caller, exactly as in the ownership chapter, and there’s nothing to dangle — tag owns real, live data.
This is a good rule of thumb: borrow what you’re given, own what you create. A function that reads existing data should take &T; a function that produces new data should return T.
The Rules of References
Everything in this chapter collapses into two rules:
- At any given time, you may have either one mutable reference, or any number of immutable references — but not both.
- References must always be valid — a reference can never outlive the value it points to.
Rule 1 stops data races and invalidated pointers. Rule 2 stops dangling references. Both are checked at compile time, and both cost nothing at runtime.
Putting It All Together
Here’s a small program using everything from this chapter. One function borrows to read, another borrows to modify, and main owns the data throughout:
fn main() {
let mut tank = vec![
String::from("shelly"),
String::from("pinchy"),
];
print_roster(&tank); // shared borrow — just reading
welcome_crab(&mut tank, String::from("bubbles")); // mutable borrow — modifying
print_roster(&tank); // shared borrow again
println!("main still owns the tank: {} crabs", tank.len());
}
fn print_roster(crabs: &Vec<String>) {
println!("--- roster ({}) ---", crabs.len());
for crab in crabs {
println!(" * {crab}");
}
}
fn welcome_crab(crabs: &mut Vec<String>, newcomer: String) {
crabs.push(newcomer);
}Output:
--- roster (2) ---
* shelly
* pinchy
--- roster (3) ---
* shelly
* pinchy
* bubbles
main still owns the tank: 3 crabsThree things worth noticing:
- Each borrow ends when its function call returns, so the borrows never overlap and the rules are satisfied automatically. This is why well-factored code tends to just… work.
welcome_crabtakesnewcomerby value, because the vector needs to keep that string — it genuinely needs ownership. Borrow when you’re only looking; take ownership when you’re storing.mainownstankfrom the first line to the last, and it’s freed exactly once, whenmainends.
Error Cheat Sheet
The four borrow errors, and what each one actually means:
| Error | What it says | What it means | Usual fix |
|---|---|---|---|
| E0596 | cannot borrow as mutable | You used & (or forgot mut on the owner) but tried to write | Add mut to the let, and &mut at the call site and in the signature |
| E0499 | cannot borrow as mutable more than once | Two &mut borrows are alive at the same time | Finish with the first before creating the second |
| E0502 | cannot borrow as mutable, also borrowed as immutable | A & and a &mut are alive at the same time | Move the last use of the shared borrows above the mutable one |
| E0106 | missing lifetime specifier | You returned a reference to something local | Return the owned value (String, not &String) |
| E0382 | borrow of moved value | You used a value after moving it | Pass &value instead of value |
Any of these can be explained in your terminal. Run rustc --explain E0502 (or any other code) for a full write-up with examples — no internet required.
Summary
| You write | What you get | Can you modify? | How many at once? |
|---|---|---|---|
value | Ownership — it moves | Yes, if mut | One owner |
&value | Shared (immutable) reference | ❌ No | Unlimited |
&mut value | Mutable (exclusive) reference | ✅ Yes | Exactly one, and no shared ones |
value.clone() | An independent copy | Yes, if mut | As many as you pay for |
And the mental shortcuts worth keeping:
&means “let me look at it”;&mutmeans “let me change it, alone.”- A borrow ends at its last use, not at the closing brace.
- Borrow what you’re given, own what you create.
- Many readers or one writer. Never both.
Frequently Asked Questions
What is a reference in Rust?
A reference is a value holding the memory address of another value, written with the & operator. It lets you read or use data without taking ownership, so the original variable stays valid. Unlike a raw pointer in C, a Rust reference is guaranteed by the compiler to always point at valid, live data — it can never be null and can never dangle.
What is the difference between borrowing and ownership in Rust?
Ownership means a variable is responsible for a value and frees it when it goes out of scope. Borrowing means temporarily accessing a value someone else owns. Pass a String by value and ownership moves — the original becomes unusable. Pass &String and the function merely borrows it: the caller keeps the value, and nothing is freed when the function ends.
Why can I only have one mutable reference at a time in Rust?
Because it makes data races and invalidated references impossible. A data race needs two pointers to the same data where at least one writes and nothing synchronizes them. By allowing either any number of read-only references or exactly one mutable reference — never both — Rust removes that possibility at compile time, with zero runtime cost. The same rule also prevents subtler bugs, like holding a reference into a Vec while pushing to it.
How do I fix “cannot borrow as mutable because it is also borrowed as immutable” (E0502)?
Finish using every immutable reference before creating the mutable one. Since a borrow ends at its last use rather than at the closing brace, moving the println! that uses your shared references above the line that creates the &mut is usually the entire fix. If you genuinely need both at once, .clone() the data so the two are independent.
What is the difference between &String and &mut String?
&String is a shared, read-only borrow — you can call .len() and print it, but not .push_str(). You can have as many as you like at once. &mut String is an exclusive borrow that permits modification, but only one may exist at a time, and no shared references may exist alongside it. Using &mut also requires the owning variable to be declared with mut.
Can a reference in Rust be null?
No. In safe Rust a reference can never be null and can never dangle — the compiler rejects any code that would let a reference outlive its value, which is what error E0106 is telling you. When you need to represent “a reference or nothing”, use Option<&T>, which is explicit and which the compiler forces you to handle.
Do references make Rust programs slower?
No. A reference is just a memory address, so passing one is as cheap as passing an integer — and far cheaper than cloning heap data. Every borrow rule is checked at compile time, so references add zero runtime overhead. Borrowing is usually the fastest option available, which is why idiomatic Rust reaches for & before .clone().
What’s Next?
You can now use values without consuming them, and change them in place without giving up ownership. That’s the everyday shape of nearly all Rust code.
There’s one more piece to the puzzle. Right now a reference borrows a whole value — the entire String, the entire Vec. But often you want a reference to just part of one: the first word of a sentence, or a few elements out of a list. Rust has a kind of reference for exactly that, called a slice, and it’s the subject of the next chapter.
In the meantime, revisit Ownership — the moves that felt restrictive there should feel a lot more reasonable now that you have & — or browse the rest of the Rust tutorial.