← all lessons
Ownership & Borrowing · #5 of 13

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.

The two kinds of reference

There are exactly two ways to borrow, and the whole lesson hangs off the difference between them:

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.

SHARED — many readersEXCLUSIVE — one writerXORsString&s&s&sread · read · readsString&mut swrite — alone
Either column, never both. Many shared readers xor one exclusive writer. This is aliasing-XOR-mutability — and it is exactly the property that makes a data race impossible to express.

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:

A shared borrow leaves the caller's value alive editable · real rustc
Open in Playground ↗ ready

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:

A mutable borrow can write through the reference editable · real rustc
Open in Playground ↗ ready

This prints hello!. Notice that the word mut appears twice, and both are required:

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:

aliasing-XOR-mutability, enforced — this WON'T compile editable · real rustc
Open in Playground ↗ ready

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:

Non-overlapping borrows are fine — this runs editable · real rustc
Open in Playground ↗ ready

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:

Returning a reference to a local — the dangling-reference error editable · real rustc
Open in Playground ↗ ready

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:

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

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.