Skip to Content
References and Borrowing

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:

the clumsy way from last chapter
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 libraryIn RustWritten as
You own the book on your shelfThe owning variablelet menu = String::from(...)
Friends read it at the table with you — as many as wantShared reference&menu
One person checks it out to annotate itMutable reference&mut menu
Everyone returns it; it’s still yoursThe borrow ends, owner unaffected
A catalog card for a book that was destroyedDangling referenceimpossible 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:

src/main.rs
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 run

Output:

'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:

  • &nickname at the call site creates a reference to nickname instead of moving it.
  • &String in the signature says “this parameter is a reference to a String, not a String.”

They have to match. A function taking &String must be given &something.

What This Looks Like in Memory

This is the diagram worth memorizing:

Memory layout of a Rust reference: on the left the reference text of type ampersand String holds a single pointer field, which points at the middle where the owner nickname holds ptr, len 6 and capacity 6, whose pointer in turn leads to the six heap bytes spelling shelly

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:

src/main.rs
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: spiral

Three 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:

src/main.rs
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 reef

Four 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:

src/main.rs
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? true

You’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:

src/main.rs
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:

src/main.rs
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:

  1. The owner must be declared mut — you can’t hand out write access to something that isn’t writable.
  2. The call site passes &mut tank_label instead of &tank_label.
  3. The parameter’s type is &mut String instead 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:

src/main.rs
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.

src/main.rs
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

The borrowing rule as two mutually exclusive panels: on the left, any number of shared ampersand menu references all read the same value; on the right, exactly one ampersand mut menu reference has write access while all other borrows are crossed out and blocked. The footer notes you can never have both at once.

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:

src/main.rs
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 2

Two &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:

src/main.rs
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 2

With 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:

src/main.rs
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:

src/main.rs
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 + shrimp

Identical 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:

src/main.rs
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 here

That 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:

src/main.rs
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.

Two-stage diagram of a dangling reference: inside make_tag the variable temp owns a String whose bytes pinchy are on the heap; after make_tag returns temp has been dropped and the heap memory freed, leaving the returned reference lost pointing at freed memory. Rust rejects this at compile time with error E0106.

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:

src/main.rs
fn main() { let tag = make_tag(); println!("{tag}"); } fn make_tag() -> String { let temp = String::from("pinchy"); temp }

Output:

pinchy

One 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:

  1. At any given time, you may have either one mutable reference, or any number of immutable references — but not both.
  2. 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:

src/main.rs
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 crabs

Three 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_crab takes newcomer by 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.
  • main owns tank from the first line to the last, and it’s freed exactly once, when main ends.

Error Cheat Sheet

The four borrow errors, and what each one actually means:

ErrorWhat it saysWhat it meansUsual fix
E0596cannot borrow as mutableYou used & (or forgot mut on the owner) but tried to writeAdd mut to the let, and &mut at the call site and in the signature
E0499cannot borrow as mutable more than onceTwo &mut borrows are alive at the same timeFinish with the first before creating the second
E0502cannot borrow as mutable, also borrowed as immutableA & and a &mut are alive at the same timeMove the last use of the shared borrows above the mutable one
E0106missing lifetime specifierYou returned a reference to something localReturn the owned value (String, not &String)
E0382borrow of moved valueYou used a value after moving itPass &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 writeWhat you getCan you modify?How many at once?
valueOwnership — it movesYes, if mutOne owner
&valueShared (immutable) reference❌ NoUnlimited
&mut valueMutable (exclusive) reference✅ YesExactly one, and no shared ones
value.clone()An independent copyYes, if mutAs many as you pay for

And the mental shortcuts worth keeping:

  • & means “let me look at it”; &mut means “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.