125. Borrowing
Moving a value into every function that wants to look at it would be exhausting. Rust lets you lend it instead, under one rule that turns Track A’s hardest problem, aliasing, into a compile error.
- Lend a value to a function with a shared reference, and keep using it afterwards.
- Change a caller’s value through a mutable reference and the * operator.
- State the many-readers-or-one-writer rule and say where a borrow ends.
- Read a borrow checker error and fix it by reordering code.
Lending, not giving
Chapter 124 left you with an awkward rule. Pass a String to a function
and it moves in. The caller loses it:
fn count(s: String) -> usize {
s.len()
}
fn main() {
let name = String::from("drip");
let n = count(name);
println!("{name} has {n} bytes");
}
That fails with borrow of moved value: name. You could have count
hand the string back, but every function that only wants to look at a
value would need that round trip.
TypeScript never has this problem. Passing an object passes an arrow to it (chapter 4), and the caller keeps its own arrow. Rust can pass an arrow too. It just keeps track of who holds one.
Lending a friend a book. They can read it, but it is still yours, and when they are done it comes back. They don’t get to throw it away, because it was never theirs to throw.
Put a & in front of the type and in front of the argument:
fn count(s: &String) -> usize {
s.len()
}
let n = count(&name);
println!("{name} has {n} bytes");
Now name is still usable after the call. &name made a reference to
the string without moving it, and when s goes away at the end of
count, nothing is dropped.
A reference &T is an address you can follow to a value of type T
that someone else owns. Making one is called borrowing. The owner
keeps ownership, so when a reference goes out of scope the value is not
dropped. Rust also guarantees that a reference always points at a valid
value[1].
Borrowing to change something
A plain & lets you read, not write. To change a borrowed value you need
a mutable reference, &mut:
fn bump(n: &mut i32) {
*n += 1;
}
let mut hits = 0;
bump(&mut hits);
bump(&mut hits);
// hits is now 2
*n follows the reference to the number it points at. (Method calls like
s.len() follow it for you, so you rarely write * there.) Notice how
loud this is. The owner says let mut, the caller says &mut hits, and
the parameter says &mut i32. In chapter 4, any function you passed an
object to could quietly change it. In Rust you can see it at the call.
Try to write through a plain & and the compiler stops you (real output
from rustc 1.97):
error[E0594]: cannot assign to `*n`,
which is behind a `&` reference
fn count(s: &String) -> usize {
s.len()
}
fn bump(n: &mut i32) {
*n += 1;
}
fn main() {
let name = String::from("drip");
let n = count(&name);
println!("{name} has {n} bytes");
let mut hits = 0;
bump(&mut hits);
bump(&mut hits);
println!("hits: {hits}");
}A shared reference &T can only read. A mutable reference
&mut T can read and write, and is made with &mut x from a variable
declared let mut. The dereference operator *r gives the value a
reference points at.
Many readers or one writer
Why not let everyone have a &mut? Because a reader who is halfway
through a value can be surprised by a writer changing it underneath.
A whiteboard in a meeting. Any number of people can read it at once. When someone picks up the pen to rewrite it, everyone else has to stop reading until they put it down, or they’ll copy down half of the old version and half of the new one.
Step through the arrows. Green ones read, the amber one writes, and the bars underneath show how long each borrow lives.
let mut s = String::from("tea");let a = &s;let b = &s;println!("{a} and {b}");let w = &mut s;w.push_str("pot");println!("{s}");
The surprise for most people is step 4. The borrows a and b don’t
last until the closing brace. They end at their last use, so w is
allowed to start straight after the println![1].
The borrowing rule: at any moment, a value may have either any
number of shared references &T, or exactly one mutable reference
&mut T, and never both. A borrow lasts from where it is made to the
last place it is used.
The borrow checker
The part of the compiler that enforces the rule is the borrow
checker. Here is what it says about the last step of the diagram
(real rustc 1.97 output, long lines wrapped, file path removed):
error[E0502]: cannot borrow `s` as
mutable because it is also borrowed
as immutable
3 | let a = &s;
| -- immutable borrow
| occurs here
4 | let w = &mut s;
| ^^^^^^ mutable borrow
| occurs here
5 | w.push_str("pot");
6 | println!("{a}");
| - immutable borrow
| later used here
Read it as three points on one timeline: where the reader started, where
the writer started, and where the reader was still needed. The fix is to
make those stop overlapping. Here, print a before you make w. Two
writers at once gets its own error, E0499, with the same three-point
shape.
fn main() {
let mut s = String::from("tea");
let a = &s;
let w = &mut s;
w.push_str("pot");
// a is still alive here: rejected.
// Move this line above `let w`.
println!("{a}");
}The arrow ^^^^^^ points at the mutable borrow, but that line is often
fine. The real problem can be the later use of the reader. Look at all
three labels before you change anything.
The borrow checker is a static analysis inside the Rust compiler. It works out where every borrow starts and where it is last used, and rejects any program where a value has a mutable borrow overlapping any other borrow. It runs at compile time, so the program it accepts has nothing left to check while it runs.
Think back to Part VI. Before Viscosity could move a field into a column,
it needed alias analysis (chapter 84) to ask: could any other arrow reach
this object? In JavaScript the answer has to be worked out, and often it
is “maybe”. In Rust, part of the answer is in the type. The Rust
Reference lists breaking the aliasing rules as undefined behaviour: while
a &mut T is live, no pointer that isn’t derived from it may read or
write that memory, and memory behind a live &T must not
change[2]. Because a correct program can’t break
those rules, the compiler is allowed to assume them. The same rule rules
out data races at compile time[1].
You will see it used in the Rust Drip VM (companion/rust). Before it
collects garbage, vm.rs builds a VmRoots value holding shared
borrows of its stack, its call frames and its globals, and hands it to
the collector. The collector can read every root, and while those &
borrows are alive, nothing can change them.
Your turn
Write order(a, b) so that afterwards *a <= *b. It takes two mutable borrows and changes the caller’s variables in place: if a holds the bigger number, swap them. It returns nothing.
fn order(a: &mut i32, b: &mut i32) {
// your code here
}Write append_twice(s, t), which adds t to the end of s twice. s is borrowed mutably and t is only read. s.push_str(x) appends x to a String.
fn append_twice(
s: &mut String,
t: &String,
) {
// your code here
}What you learned
- A reference
&Tlets you use a value without owning it. Making one is borrowing, and dropping a reference never drops the value. - A mutable reference
&mut Tcan change the value, through*r. It needslet muton the owner and&mutat the call. - The rule: many readers or one writer, never both at once. A borrow ends at its last use.
- The borrow checker enforces the rule at compile time. Its errors name where each borrow starts and where it is still needed.
- A
&mutis a promise of no aliases, the very fact Viscosity had to prove for itself in TypeScript.
Next: slices. A reference to part of an array or a string, which is how the Rust Drip scanner reads its source without copying a byte.
Check your understanding
0/6- 1.
Does this compile?
let mut s = String::from("a"); let r1 = &s; let r2 = &s; println!("{r1}{r2}"); - 2.
Does this compile?
let mut n = 1; let a = &mut n; let b = &mut n; *a += 1; - 3.
Does this compile?
let mut s = String::from("tea"); let a = &s; println!("{a}"); let w = &mut s; w.push_str("pot"); - 4.
What does the borrow checker do?
- 5.
fn show(s: &String)is called asshow(&name). What happens tonamewhenshowreturns? - 6.
Compared with TypeScript, what does
&mutat a call site, likeorder(&mut x, &mut y), tell you?
References
- [1] Steve Klabnik, Carol Nichols, Chris Krycho (2026). The Rust Programming Language (3rd edition). No Starch Press; free online at doc.rust-lang.org/book. doc.rust-lang.org/book/ (checked 2026-10-31)
- [2] The Rust Reference contributors. Behavior considered undefined. The Rust Reference. doc.rust-lang.org/reference/behavior-considered-undefined.html (checked 2026-10-31)