Borrowing — Reading Data Without Taking It
References, &mut, and the one rule that prevents data races
The last lesson left you with a problem: in Rust, handing a value to a function gives it away. If every read cost you ownership, the language would be unbearable.
The escape hatch is one of the most elegant ideas in programming — borrowing — and it rests on a single rule so simple you can hold it in one breath.
The previous lesson ended on a cliffhanger. Passing a String into a function moves it, so the caller loses it. If reading anything required a clone or a permanent handoff, Rust would be exhausting to write — you’d be making copies all day just to print a length.
The fix is borrowing: giving a function a temporary peek at a value without giving up ownership. Borrowing is how Rust functions are normally written. Once you can read references in a function signature, you can read most Rust code in the wild.
A few more words you’ll need
These come up constantly here. Get the rough idea now; we’ll see each in action.
- Reference: a pointer to a value that someone else owns. Written
&value. The reference doesn’t own the data — it just points at it, and it goes away without freeing anything. - Borrow: the act of creating a reference. We say a function “borrows” a value.
- Thread: a single sequence of instructions the computer runs. Modern programs often have many threads going at once (a web server handling thousands of requests; a video editor playing back and rendering in parallel). Threads share memory by default — which is what makes them powerful and dangerous.
- Data race: a bug where two threads touch the same memory at the same time and at least one is writing. The result is undefined — could be one value, the other, or a corrupted mixture. Data races are notoriously hard to debug because they only show up sometimes.
- Dangling reference: a reference that points at memory which has already been freed. Reading through it is use-after-free — the bug from the last lesson, wearing a different hat.
The two kinds of reference
There are exactly two ways to borrow, and the whole lesson hangs off the difference between them:
&T— a shared (read-only) reference to a value of typeT. You can have any number of these at the same time. Everyone can read; nobody can write.&mut T— an exclusive (read-write) reference. You can have only one at a time, and no shared references may exist while it does.
The phrase to memorise: at any moment, either many &T, or exactly one &mut T, never both.
That single rule is what makes Rust safe without a garbage collector. The picture below is the rule made visible. The owner s sits in the middle. On the left, the world is allowed to crowd in with as many shared readers as it likes. On the right, a single &mut writer takes the value alone. You are always in one column or the other — never astride both.
By statically forbidding shared and mutable access at the same time, Rust rules out data races by construction — at compile time, with no runtime cost. The compiler component that enforces this is the borrow checker you keep hearing about. When it rejects your code, it is saving you from a class of bug that other languages either accept silently or crash on at runtime.
A shared borrow: read it, give it back
Here is the move problem from last lesson, solved. The function takes &String instead of String, so it only borrows. At the call site we write &name to create the reference. After the call returns, name is still ours — intact. Hit Run:
Two characters did all the work: & in the parameter type (&String instead of String) and & at the call site (&name instead of name). The function got a temporary peek; ownership never left main. This is the single most common shape in real Rust — a function that reads its input and hands it back.
A mutable borrow: change it in place
A shared borrow can only read. To let a function change the value, you borrow it mutably with &mut:
This prints hello!. Notice that the word mut appears twice, and both are required:
let mut greeting— the original binding must be mutable, because we’re about to allow modifications through a borrow.&mut greeting— the borrow itself must say&mut, declaring “I want to write, not just read.”
If you drop either mut, the compiler stops you. Rust never lets a write happen by accident.
The rule in action — and the error you must learn
Now the heart of the lesson. You cannot hold a &mut reference while a shared & reference to the same value is still alive. This is the rule the diagram drew. Hit Run and read the error carefully — this is one you will meet again and again:
The compiler answers with **error[E0502]: cannot borrow \tag` as mutable because it is also borrowed as immutable**. As far as it's concerned, allowing both would let writermutatetagwhilereader` still points at it — the exact data-race shape. Rust refuses to even let that be expressible. The error names the immutable borrow, the conflicting mutable borrow, and the later use that keeps the first one alive. Learn to read those three lines; they are the borrow checker explaining itself.
The checker is smarter than the braces
A reference doesn’t live until the end of its block. It lives only until its last use. So the version below — almost identical to the one that just failed — compiles, because reader is done before writer is born:
This prints draft then draft v2. The shared borrow ended at its last println!, so the mutable borrow has the field to itself. This “a borrow ends at its last use, not the end of the block” behaviour has a name — non-lexical lifetimes, or NLL — but you don’t need to memorise it. The practical takeaway: if you stop using one borrow before the next one starts, you are fine.
Why this deletes a whole bug class
Here’s a bug that runs silently in Python: mutating a list while you iterate it.
v = [1, 2, 3]
for x in v:
v.append(x) # modifies v while looping over it — chaos
In C++ the equivalent corrupts memory and is undefined behaviour. In Rust, the same shape simply doesn’t compile — for x in &v holds a shared borrow for the whole loop body, and v.push(...) wants a mutable one. Rule violated, compile error, before the program ever runs. The bug you’d have shipped in Python or C++ becomes a 30-second fix in Rust.
And the dangling-reference problem disappears the same way. Try to return a reference to a value that’s about to be destroyed, and the borrow checker stops you cold. Hit Run:
The compiler answers with error[E0106]: missing lifetime specifier — its way of saying “this returned reference points at something with no owner left alive.” local is dropped when make_ref returns, so the reference would point at freed memory: a textbook use-after-free. In C this compiles and is a security hole. In Rust it’s a compile error you fix in seconds (here, by returning the owned String itself — drop the &).
This pattern repeats throughout the language. Use-after-free, double-free, iterator invalidation, data races — these aren’t bugs you try to avoid in Rust. They’re bugs that cannot be expressed in code that compiles.
Reading function signatures fluently
A huge part of becoming productive in Rust is glancing at a signature and instantly knowing what it does with its inputs:
fn f(s: String)— takes ownership ofs. The caller loses access to it.fn f(s: &String)— borrowssshared. The caller keeps it;fcan only read.fn f(s: &mut String)— borrowssmutably. The caller keeps it;fcan read and write, and no other reference tosmay exist whilefruns.
In real code you’ll also see &str instead of &String, and &[T] instead of &Vec<T>. Those are slice types — the topic of the next lesson — and they’re how most Rust functions actually take text and list inputs.
Why &mut is really about exclusivity, not mutation
It’s tempting to read &mut as “the mutable reference,” but the deeper truth is that &mut means the exclusive reference — the one borrow with the guarantee that nothing else can see this value right now. Mutation is merely what that exclusivity makes safe. This is why the rule is stated as aliasing-XOR-mutability rather than just “one writer”: the danger isn’t writing as such, it’s writing while someone else is reading or writing the same memory. Give a writer exclusive access and the danger is gone. The compiler leans on this so hard that &T and &mut T carry different optimization promises down to the machine-code level — the compiler can assume a &mut T truly is the only path to that value, and reorder around it accordingly. Borrowing isn’t just a safety feature bolted on top; it’s information the optimizer gets to use.
Key takeaways
- A reference points at someone else’s data without owning it. Two kinds: shared (
&T) and exclusive (&mut T). - The borrow rule (aliasing-XOR-mutability): at any moment, either many
&Tor exactly one&mut T, never both. The borrow checker enforces this at compile time. - A mutable binding (
let mut) is required before you can take a&mutof it. Bothmuts must be present. - Borrows live from creation to last use, not to the end of the lexical scope (non-lexical lifetimes), so non-overlapping borrows are fine.
- Violations surface as named errors —
E0502for shared-vs-mutable conflicts,E0106for a reference with no owner left alive. They rule out data races, iterator invalidation, and use-after-free before the program runs. - Reading signatures (
Stringvs&Stringvs&mut String) tells you exactly what a function does with its input. Practice until it’s automatic.
Many readers, or one writer, never both. From that single line Rust derives an entire category of guarantees — no data races, no dangling pointers, no use-after-free — all proven before your program ever starts.
The next lesson zooms in on the references you’ll actually write most: slices, the borrowed views into strings and lists that make this rule feel effortless.