Last lesson ended on a cliffhanger. Taking ownership of a value just to hand it back so the caller can keep using it works, but it's clunky — and clunky function signatures are usually a sign that a better tool exists for the job. There is one here, and it might be the single idea that makes Rust feel less like a trade-off and more like a genuine upgrade: the reference.
What a reference actually is
A reference is a way to point at a value without owning it — Rust's answer to "I just need to look at this for a second, I'm not trying to keep it." You create one with &, and in exchange, the compiler guarantees that for as long as your reference exists, the thing it points to is still there and hasn't shifted out from under you in some surprising way.
We call the act of creating a reference borrowing — and the metaphor holds up better than most programming analogies do. You can read a borrowed book, even scribble in the margins if you asked first, but you give it back eventually, and you'd better not be the only one allowed to write in it while somebody else is reading it at the same time.
Borrowing instead of taking
Here's the fix for the awkward "take it, then hand it back" pattern from the ownership lesson:
Walk through it slowly. &s1 creates a reference to s1 — pointing at its data without taking ownership of it. The parameter type s: &String says "I'll take a reference to a String, thanks," and because s never owns what it's looking at, there's nothing for it to drop when it goes out of scope at the end of the function. Meanwhile, back in main, s1 is completely untouched — you can keep using it in that println! right after the call, which would have been a flat-out compile error if we'd passed s1 itself, the way the previous lesson did.
That's the whole trick. calculate_length gets to peek at the string, compute its length, and hand control straight back — without ever taking the string away from its owner.
Mutable references: opt-in, on purpose
References, like variables, are immutable by default. If you want to modify something through a reference, every link in the chain has to explicitly agree to it:
Three separate muts have to line up for this to compile: the original binding (let mut s), the reference you create at the call site (&mut s), and the type the function declares it accepts (&mut String). Leave any one of them out and the compiler stops you — not as a punishment, but because each one is a deliberate signal to the next reader: "yes, this value can and will change, right here."
The rule that makes Rust fearless
Now for the part that catches newcomers off guard. Rust places a hard limit on mutable references — and once you see why, you'll probably agree it's one of the better decisions baked into the language. At any given moment, for any particular piece of data, you can have either:
- any number of immutable references (
&T), or - exactly one mutable reference (
&mut T)
— but never both at once, and never more than one mutable reference at a time. Try to bend that rule, and the compiler catches you immediately:
That second line claims a brand-new, exclusive write-lock on s while the first one is still active — and if the compiler allowed it, both r1 and r2 would believe they alone could safely modify the same data. Whichever one happened to run last would silently overwrite the other's work.
Mixing the two kinds is just as forbidden — and this is the one that sneaks up on people most in real code:
Notice how specific that message is — it doesn't just say "no." It names the exact line where the conflicting borrow began, and the exact line where it's still in use afterward. This is a pattern you'll keep noticing with Rust's compiler: it isn't trying to slow you down, it's trying to make sure an entire class of bug never ships.
That class of bug has a name worth knowing: a data race — when two pointers access the same memory at the same time, at least one of them is writing, and nothing coordinates between them. In most languages, a data race is the kind of bug you discover from a pager alert at 3am, after it's been silently corrupting data in production for a week — and it slips through your test suite nine times out of ten, because timing-dependent bugs are exactly that sneaky. In Rust, it's a compile error. Full stop. On your laptop. Before you've run the program even once. (File that thought away — it's the entire reason fearless concurrency, much later in this course, gets to put the word "fearless" in its name.)
The checker is smarter than you'd expect
You might be bracing for this rule to make ordinary code awkward — "but what if I just need to read something quickly before I change it?" Good news: the borrow checker doesn't reason in terms of where your { } blocks open and close. It tracks where each reference is actually last used:
r1 and r2's borrows effectively end the instant that first println! finishes — their final use — so creating r3 immediately afterward is completely fine, even though, visually, everything sits inside the same block. This behavior even has an official name (non-lexical lifetimes), but you don't need to memorize it — just trust that the compiler is reasoning about what your code does, not how it happens to be indented.
Dangling references: a bug Rust makes impossible
In languages with manual memory management, this is a famous, easy-to-write mistake: keep a pointer around after whatever it pointed to has been freed, and you've got a "dangling pointer." Read through it and you'll get garbage data, a crash, or — worse — something that looks completely fine for months until it very much isn't.
Rust's compiler refuses to let this category of bug exist in the first place. Try to write one, and your program won't even compile:
Read that help line again — the compiler isn't being cryptic, it's explaining exactly what's wrong. s is created inside dangle, so the instant that function ends, s is dropped and its memory freed. Returning &s would hand the caller an address pointing at memory that no longer belongs to anyone — precisely the dangling pointer we just described, except Rust spotted it before your code ever ran. The fix is simple: don't return the reference. Return the value itself, and let ownership move out to the caller — exactly what gives_ownership did back in the ownership lesson.
The borrow checker, in two rules
Take a breath — that's genuinely the whole system. It boils down to two rules, and you've now seen both of them in action:
- At any given time, you can have either one mutable reference or any number of immutable references — never both.
- References must always point at something valid. No exceptions, no dangling — the compiler enforces this before your program ever runs.
Every confusing borrow-checker message you run into from here on out is just one of these two rules, applied with total consistency, explained in increasingly specific language as the compiler keeps getting better.
Quick exercise
Try writing a function fn first_word_length(s: &String) -> usize that returns the length, in bytes, of the first word in a string — everything up to the first space, or the whole length if there's no space at all. You already have what you need: string methods, &String, and a loop.
...Although — hold that thought before you go too far down that road. There's a sharp edge hiding inside that exercise (what happens to the number you returned if the string changes right after you compute it?), and Rust has a purpose-built tool that makes the entire problem disappear before it starts. It's called a slice, and it's exactly what's waiting in the next lesson.