Viscosity
Review
Part IX · Rust for TypeScript developers · Day 32

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.

About 18 minutes, including the exercises and quiz
By the end you will be able to
  • 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.

Intuition

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.

Definition

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
Try itLend it, then use it again
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}");
}
Open in Rust Playground ↗
Definition

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.

Intuition

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.

Step throughMany readers or one writer
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}");
References a, b and w point at the String s. Shared borrows are one colour, the mutable borrow another; a bar chart shows how long each borrow lives.BORROWSOWNERs: String"tea"HOW LONG EACH BORROW LIVES (code lines 2–7)234567abw
s owns the string. Nobody else can see it yet.
1/8

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].

Definition

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.

Try itA reader and a writer at once
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}");
}
Open in Rust Playground ↗
Pitfall

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.

Definition

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.

Under the hood

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

ExercisePut two numbers in order, in place

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
}
Open in Rust Playground ↗
Extra practiceOne writer, one reader

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
}
Open in Rust Playground ↗

What you learned

  • A reference &T lets you use a value without owning it. Making one is borrowing, and dropping a reference never drops the value.
  • A mutable reference &mut T can change the value, through *r. It needs let mut on the owner and &mut at 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 &mut is 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. 1.

    Does this compile?

    let mut s = String::from("a");
    let r1 = &s;
    let r2 = &s;
    println!("{r1}{r2}");
  2. 2.

    Does this compile?

    let mut n = 1;
    let a = &mut n;
    let b = &mut n;
    *a += 1;
  3. 3.

    Does this compile?

    let mut s = String::from("tea");
    let a = &s;
    println!("{a}");
    let w = &mut s;
    w.push_str("pot");
  4. 4.

    What does the borrow checker do?

  5. 5.

    fn show(s: &String) is called as show(&name). What happens to name when show returns?

  6. 6.

    Compared with TypeScript, what does &mut at a call site, like order(&mut x, &mut y), tell you?

References

  1. [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. [2] The Rust Reference contributors. Behavior considered undefined. The Rust Reference. doc.rust-lang.org/reference/behavior-considered-undefined.html (checked 2026-10-31)