Learning Rust · Part III · Chapter 03

Borrowing and References

III.02 left you giving a value away just to look at it and handing it back. This chapter is the fix: a reference lets you borrow a value, read it or tweak it, and leave the owner holding it the whole time.

01

Look at it, do not take it

The previous chapter ended on a complaint, and it was a fair one. The strict ownership rules made you move a value into a function just to read it, and then move it back out if the caller still wanted it. That give-it-back dance was clumsy on purpose, because most of the time you do not want to own a value to use it. You want to look at it (or tweak it) and let the owner keep it. That is exactly what this chapter delivers. The tool is the reference: a borrowed view of a value that does not take ownership. You make one with &, read English “a reference to” or simply “borrow”. Run this:

look_without_taking.rs
// III.02 left you moving a value into a function just to read it, then moving
// it back out. A shared reference lets you look without taking: the owner keeps
// the value, and the function borrows a read-only view of it.
fn length_of(text: &String) -> usize {
    // `text` is a &String: a borrowed view, not an owner. We may read through it.
    text.len()
}

fn main() {
    let banner = String::from("ferris was here");

    // `&banner` lends the value to length_of without giving it away.
    let n = length_of(&banner);

    // banner is still ours: the borrow ended when length_of returned.
    println!("{banner:?} is {n} bytes, and we still own it");
}

The function takes &String, a reference to a String, not a String. At the call site length_of(&banner) hands over a borrow: a pointer to banner, not banner itself. The function reads through it, returns a number, and the borrow ends. Because nothing was moved, banner is still fully usable on the last line. Compare this with III.02, where passing the String by value would have moved it and left banner dead. A reference is the polite version of a function call: lend me this, I will give it right back.

A reference borrows; it does not own

&value creates a reference: a borrow of the value that points at it without taking ownership. Passing a reference to a function lends the value for the duration of the call; the owner keeps it and gets it back the instant the borrow ends. No move happens, so the source binding stays alive. This is the everyday way to pass data around in Rust when you are not deliberately transferring ownership.

Why borrow when you could just clone?

III.02 warned you not to sprinkle .clone() on move errors, because a clone allocates and copies. A reference is the alternative it was pointing at. Borrowing copies nothing: &banner is a single machine word, a pointer, no matter how large the String behind it. When you only need to read or change a value in place, a borrow is free where a clone is not. Reach for the borrow first; clone only when you genuinely need a second, independent owner.

02

Shared references: many readers at once

References come in two kinds, and the first is the one you just used. A shared reference, written &T, is a read-only borrow. You can look at the value through it, call methods that read, print it, but you cannot change it. The defining property is in the name: you may hand out as many shared references as you like at the same time, all pointing at one value, because none of them can modify it. Many readers never conflict. Run this:

many_readers.rs
// A shared reference is written &T. You may hand out as many at once as you
// like, because none of them can change the value: reading is always safe to
// share. The owner stays put the whole time.
fn main() {
    let total = 1000_u32;

    let a = &total; // a shared borrow
    let b = &total; // another, at the same time: perfectly fine
    let c = &total; // and a third

    // All three view the same value. None can mutate it, so sharing is safe.
    println!("through a: {a}");
    println!("through b: {b}");
    println!("through c: {c}");
    println!("the owner still has it: {total}");
}

Three shared references, a, b, and c, all borrow total at once, and the program is perfectly happy. They are read-only windows onto the same number, and the owner total is still readable too. If you come from Go or Java, this is the intuition you already have for passing a pointer to a function that only reads: nothing is copied, everyone sees the same data, and because nobody writes, nobody steps on anybody. Rust just makes the read-only part explicit in the type: &T cannot write, full stop.

&T is a shared, read-only loan

A shared reference &T lets you read the value but never change it. Any number of shared references to the same value may exist at once, because read-only access is always safe to share. This is why &T is sometimes called an immutable reference: through it, the value cannot be mutated, no matter how many copies of the reference are floating around.

03

Exclusive references: exactly one writer

The second kind is the one that earns its keep, because it lets you change a value through a borrow without owning it. A mutable reference, written &mut T, is a writable borrow. Where &T only reads, &mut T can modify the value it points at. There is one firm condition: to borrow a value mutably, the binding you borrow from must itself be declared mut, because you are about to change what it owns. Run this:

one_writer.rs
// A mutable reference is written &mut T. It lets you change the value through
// the borrow. To take one, the binding you borrow from must itself be `mut`.
fn bump(counter: &mut u32) {
    // `*counter` reaches through the reference to the value it points at.
    *counter += 1;
}

fn main() {
    let mut hits = 0_u32; // the owner must be mut to be borrowed mutably
    println!("before: {hits}");

    bump(&mut hits); // lend an exclusive, writable view for the call
    bump(&mut hits); // the borrow from the first call already ended

    // After both calls the borrows are over and we own `hits` outright again.
    println!("after:  {hits}");
}

bump takes &mut u32, a mutable borrow of a number, and increments it through the reference. In main, hits is declared mut, so &mut hits is allowed, and after the two calls the original hits reads 2: the function changed the caller’s value in place, with no move and no return. This is the “tweak it and give it back” half of the chapter’s promise. The function borrowed hits mutably, did its work, and the borrow ended, leaving main the owner of the now-changed value.

But &mut T carries a restriction that &T does not, and it is the heart of the whole chapter. While a mutable reference to a value exists, it must be the only live reference to that value: no other &mut, and no & either. That is why the truer name for it is an exclusive reference. A shared reference shares; an exclusive reference excludes everyone else for as long as it lives. The next two sections show what that buys you and what the compiler does when you break it.

&mut T is an exclusive, writable loan

A mutable reference &mut T lets you change the value it borrows, and while it is alive it is the only reference to that value that may exist. To take one, the owning binding must be declared mut. “Mutable” describes what you can do (write); exclusive describes the rule it enforces (no other borrow at the same time). They are two names for the same &mut T.

04

Dereferencing: reaching the value with *

A reference points at a value; sometimes you need the value itself, not the pointer. The operator that follows a reference back to what it points at is the star, *, and the act is called dereferencing. If r is a reference, then *r is the value behind it: you read through it with *r on the right of an assignment, and you write through it with *r on the left. Run this:

follow_the_star.rs
// The `*` operator dereferences a reference: it follows the pointer to the
// value. You read and write the pointed-to value through the star.
fn main() {
    let mut score = 10_i32;

    let view = &score; // shared reference
    // Reading: *view is the i32 behind the reference.
    println!("score seen through a shared ref: {}", *view);

    let edit = &mut score; // exclusive reference
    *edit += 5; // writing: change the value the reference points at
    println!("after writing through &mut: {}", *edit);

    // Most of the time println! and method calls deref for you, so you write
    // `score` and `view.len()` without a star. The star is always there for
    // when you want to be explicit about reaching the value itself.
    println!("the owner now reads {score}");
}

Through the shared reference view, *view reads the i32 behind it. Through the exclusive reference edit, *edit += 5 writes to the value it points at, and the owner score sees the change. The star is the same idea you know from pointers in C or Go (*p), just governed by the borrow rules so it can never point at freed or aliased memory. If you have written Go, read & as “address of” and *as “value at”; the spellings match and the safety is what is new.

Rust often inserts the * for you

You will write far fewer stars than this example suggests, because Rust dereferences automatically in the common cases. println!("{view}") reads through view without a star, and a method call like text.len() on a &String auto-dereferences to reach the method. This convenience is called auto-deref, and it is why borrowed values usually read just like owned ones. The explicit * is always there when you need to be precise, above all when writing through a &mut.

05

The one rule: aliasing XOR mutability

Everything so far converges on a single rule, and it is the rule the borrow checker exists to enforce. At any given moment, for any given value, you may have either any number of shared references, or exactly one exclusive reference, but never both at once. Read-only sharing or a single writer, never a writer alongside a reader. It is sometimes phrased aliasing XOR mutability: you can alias a value (have many references to it) or you can mutate it through a reference, but you cannot do both to the same value at the same time. Here is what happens when you try. The program below does not compile; press Run and read the diagnostic rather than fixing it:

alias_then_mutate.rs
// This program does NOT compile. It is here so you can read the borrow-checker
// error. Press Run and look at the diagnostic: error[E0502].
fn main() {
    let mut data = vec![1, 2, 3];

    let peek = &data; // a shared borrow of data starts here and stays live

    // Trying to take a &mut while `peek` is still in use breaks the central
    // rule: you cannot have a shared borrow and an exclusive borrow at once.
    data.push(4); // needs &mut data, but data is already borrowed by `peek`

    println!("{peek:?}"); // `peek` is used here, so its borrow had to live this long
}

The error is error[E0502]: cannot borrow `data` as mutable because it is also borrowed as immutable. The shared borrow peek is still alive when data.push(4) asks for a mutable borrow (a push needs &mut, since it changes the Vec), so the two collide: a reader and a writer at the same instant, which the rule forbids. The compiler points at the shared borrow, at the attempted mutation, and at the later use of peek that kept it alive. No crash, no race, just a refusal to build.

This rule is not bureaucracy; it is the thing that makes Rust’s safety promises hold. Two of them in particular. First, data races: a data race is two threads touching one value at once with at least one writing, and it is undefined behaviour in most languages. The exclusive-writer rule makes a data race impossible to even write, because you cannot hand a writer and a reader to the same value at the same time, on one thread or across many. Second, iterator invalidation: in Go or C++ you can append to a collection while iterating over it, and a reallocation can leave your iterator pointing at freed memory, a bug you find at runtime. In Rust the iterator is a shared borrow of the collection, so a mutation that would invalidate it cannot be borrowed while the iterator lives. The bug is a compile error instead of a 3am page.

Aliasing XOR mutability, in one line

For any value, at any moment: many readers, or one writer, never both. Any number of &T may coexist (they only read), or exactly one &mut T may exist (it can write), but a &mut T may not share the stage with any other reference. This single rule is what rules out data races and iterator invalidation at compile time, with nothing to check at runtime.

A reference may never outlive what it points at

The rule has a companion: a reference must never outlive the value it borrows. A reference that points at a value that has already been dropped is a dangling reference, the borrowed cousin of a use-after-free, and Rust rejects it too. Try to return &x from a function where x is a local (it would be dropped when the function ends) and the compiler stops you with E0515, “cannot return reference to local variable”, or asks for a lifetime with E0106. You cannot borrow something into the void; the borrow checker ties every reference to a value that still exists. Howit names that tie is III.05’s job, not this chapter’s.

06

A borrow ends at its last use, not at the brace

The E0502 in the last section might have left you worried that a borrow ties up a value for the whole block it lives in. It does not, and the precise answer is what makes borrowing pleasant in practice rather than a straitjacket. A borrow ends at its last use, the final line where the reference is actually read, not at the closing brace of its scope. The compiler tracks exactly how far each reference reaches and frees the borrow the moment the reference is done. Run this:

last_use_wins.rs
// A borrow ends at its LAST use, not at the closing brace. This is called
// non-lexical lifetimes: the compiler tracks where a reference is really done.
fn main() {
    let mut tally = vec![10, 20, 30];

    let view = &tally; // shared borrow begins
    println!("snapshot before the change: {view:?}");
    // `view` is never touched again, so its borrow is OVER right here, even
    // though we are not at the end of the block.

    // Because the shared borrow already ended, taking &mut tally is allowed.
    tally.push(40);
    println!("after the change: {tally:?}");
}

The shared borrow view is created, used once in the println!, and never touched again. So even though view is still in scope on the next line, its borrow is already over, and tally.push(40) is free to take a mutable borrow. Move the read of view down below the push and you would recreate the E0502 from the last section, because then the shared borrow would still be live when the mutation wanted exclusivity. The borrow lasts exactly as long as you use it, and not a line longer.

Demystifying “non-lexical lifetimes”

The official name for “a borrow ends at its last use” is non-lexical lifetimes, often abbreviated NLL, and it is a mouthful that hides a simple idea. Lexical would mean tied to the text of the block, ending at the closing brace; non-lexical means tied to actual use instead, ending where the reference is last read. Early Rust really did end borrows at the brace, and it was annoying; modern Rust ends them at last use, which is why code like this one just works. You do not configure NLL or write anything for it; it is simply how the borrow checker has reasoned since 2018.

07

Passing borrows onward, and method receivers

Two everyday patterns finish the picture, and both are just the rules you have already met, applied one level down. The first is what happens when you pass a &mut you were handed to another function. You do not give the exclusive borrow away (you only borrowed it yourself); you lend it onward for the length of that call. This is called a reborrow. Run this:

lend_it_onward.rs
// Passing a &mut to another function is a REBORROW: the callee borrows from
// your borrow for the length of the call, then control (and exclusivity)
// returns to you. You never gave the &mut away, you lent it onward.
fn add_tax(price: &mut u32) {
    *price += *price / 10; // a 10% tax, written through the reference
}

fn checkout(cart_total: &mut u32) {
    println!("subtotal: {cart_total}");
    add_tax(cart_total); // reborrow: lend our &mut to add_tax for this call
    println!("with tax: {cart_total}"); // exclusivity is ours again here
}

fn main() {
    let mut total = 200_u32;
    checkout(&mut total); // hand one &mut down the chain
    println!("charged: {total}");
}

checkout holds a &mut u32 it was lent, and passes it to add_tax, which also wants a &mut u32. That is a reborrow: add_tax borrows from checkout’s borrow for the duration of the call, and when it returns, exclusivity flows back to checkout, which goes on to read the updated total. The exclusive rule is never violated, because at every instant exactly one borrow is the live writer; the loan just moves down the call chain and back up. You write add_tax(cart_total) with no extra syntax, and Rust inserts the reborrow (&mut *cart_total) for you.

The second pattern is the one you will read on every method you ever define: the receiver. A method’s first parameter describes its relationship to the value it is called on, and it is exactly the three options from this chapter and the last. Run this:

receivers.rs
// A method's receiver says what it does to the caller's value:
//   &self     borrows it to read,         caller keeps it usable
//   &mut self borrows it to change,       caller keeps it usable
//   self      takes ownership (moves it), caller loses it
struct Account {
    balance: u32,
}

impl Account {
    // &self: read-only borrow. The caller still owns the account afterward.
    fn report(&self) -> u32 {
        self.balance
    }

    // &mut self: mutable borrow. Changes the caller's account in place.
    fn deposit(&mut self, amount: u32) {
        self.balance += amount;
    }

    // self: takes ownership. The account is consumed and cannot be used again.
    fn close(self) -> u32 {
        println!("closing account, returning the final balance");
        self.balance
    }
}

fn main() {
    let mut acct = Account { balance: 100 };

    acct.deposit(50); // &mut self: borrows, mutates, hands the account back
    println!("balance is now {}", acct.report()); // &self: borrows to read

    let final_balance = acct.close(); // self: moves acct into close
    println!("account closed with {final_balance}");
    // acct is gone here: close took ownership, exactly like III.02's moves.
}

Read the three methods as three contracts with the caller. fn report(&self) takes &self, a shared borrow of the account: it reads, and the caller keeps the account fully usable. fn deposit(&mut self, ...) takes &mut self, an exclusive borrow: it changes the account in place, and again the caller keeps it, now modified. fn close(self) takes self by value: it moves the account into the method, exactly the move from III.02, so after acct.close() the caller can no longer use acct. The receiver is the type signature telling you, at a glance, whether a method reads (&self), writes (&mut self), or consumes (self) the value you call it on.

The receiver tells the caller what to expect

&self borrows the value to read it; the caller keeps it. &mut self borrows it to change it; the caller keeps it, changed. self takes ownership and moves the value into the method; the caller loses it. The same three relationships, read / write / consume, that govern every reference and move in Rust, written once at the front of each method.

08

The rules the borrow checker enforces, stated plainly

You have now seen every piece, so here is the whole of borrowing written down. To use a value without taking it, you borrow it with &, producing a reference that points at the value while the owner keeps it. There are two kinds. A shared reference &T is read-only, and you may have any number of them to one value at once. A mutable, exclusive reference &mut T can write, and while it lives it must be the only reference to that value. You reach the value behind a reference with the dereference operator *, though Rust inserts it for you in most reads and method calls.

Over all of that sits one rule: for any value, at any moment, you may have many shared references or one exclusive reference, never both. Aliasing XOR mutability. The borrow checker enforces it, rejecting a &mut that overlaps with any other borrow (E0502) and a reference that would outlive what it points at (E0515 / E0106). And a borrow lasts only until its last use, not until the closing brace, which is what keeps the rule from getting in your way. That single rule is what rules out data races and iterator invalidation at compile time, for free.

Two threads run ahead of here on purpose. First, this chapter borrowed whole values: a &String, a &mut Vec. Often you want to borrow only a part, the middle of an array or the first word of a string, and that borrowed window is a slice. Slices are III.04, the very next chapter, and they are just references with a length. Second, every reference here had its life tracked silently by the compiler. When you return a reference or store one in a struct, you sometimes have to name how long it lives, and that name is a lifetime. Naming lifetimes is III.05. For now, trust that the borrow checker ties every borrow to a value that still exists.

The whole chapter in six lines

  1. A reference borrows, it does not own. &value lends a value out so you can read or change it while the owner keeps it; no move happens and nothing is copied.
  2. &T is shared and read-only; you may have many at once. &mut T is exclusive and writable, and the binding it borrows from must be mut.
  3. The dereference operator * reaches the value behind a reference, to read or to write, though Rust inserts it for you in most reads and method calls.
  4. Aliasing XOR mutability: for any value, many shared references or one exclusive reference, never both. Breaking it is E0502; this rule is what rules out data races and iterator invalidation.
  5. A borrow ends at its last use (non-lexical lifetimes), not at the closing brace, and a reference may never outlive what it points at (E0515 / E0106).
  6. A method receiver picks one of three: &self to read, &mut self to change, self to consume. A reborrow lends a &mut onward for a call and gets exclusivity back when it returns.
End-of-chapter assessmentCheck your understanding20 questions. Your first attempt is recorded on your report card.Take it →