The absurd version: handing it back just to look
The previous chapter left you with a rule that sounds reasonable until you try to live inside it. When you pass a value to a function, you move it: ownership leaves the caller and follows the value in. The function now owns it, and the moment the call returns, the original variable is gone. That is fine when the function is meant to consume the value, take a String and write it to a file, say, and you have no further use for it. But most functions are not like that. Most functions just want to look: count the characters, check a flag, read a field. And under a strict move rule, looking and consuming are the same act, because both start by handing the value over.
Follow that to its conclusion and it gets absurd. If a function that only reads has to take ownership to see the value at all, then the only way for the caller to keep using its own data is for the function to hand it back. A length function cannot just return the length; it has to return the length and the value, bundled together, so the caller can rebind the value and carry on. You moved a String across town to count its letters, and then the function had to mail it home. The program below does exactly this. It compiles, it runs, and it is ridiculous:
// Read a value by MOVING it in, then hand it back so the caller can keep using it.
// No real Rust program is written this way; the next section fixes it with a reference.
fn length_then_return(name: String) -> (usize, String) {
let len = name.len(); // we only wanted this number
(len, name) // but we have to give the String back, or the caller loses it
}
fn main() {
let name = String::from("hello");
// The call MOVES `name` into the function. To keep using the name afterward,
// we have to catch the returned String and rebind it to a variable.
let (len, name) = length_then_return(name);
println!("{name} has length {len}");
println!("still have: {name}");
}Read the return type first, because that is where the absurdity lives. length_then_return wants to give you one number, a usize, but its signature says it returns (usize, String), a pair. The second half of that pair is pure overhead: it is the original String being shipped back so the caller does not lose it. At the call site you see the cost spelled out. let (len, name) = length_then_return(name) tears the pair apart and rebinds name to the very value you started with, just so the two println! lines below can still use it. Every read would carry this tax: take the value, do the trivial work, package the value back up, return it, unpack it on the other side.
If you come from Go, this is the moment the discomfort gets sharp, because in Go you would never think about any of this. You write func length(s string) int, you pass the string, you read its length, and the caller’s variable is untouched, because Go passed a copy and the original stayed put. You have done this ten thousand times without a thought. The Rust rule from the last chapter seems to take that ordinary, free act of looking at a value and turn it into a logistics problem. That cannot be the real story, and it is not.
The resolution has a name, and you have already used the word without meaning anything technical by it: borrowing. The next section introduces a way to let a function access a value without taking ownership of it, so the owner stays the owner, nothing has to be handed back, and the read costs nothing but a glance. The hand-it-back program is here only so you feel the problem in your hands before the fix arrives. Once you have the real tool, you will never write a signature like (usize, String) again, and you will understand exactly why no Rust programmer does.
After the last chapter, passing a value moves ownership out of the caller. So a function that only wants to read would have to return the value too, just to give it back. That awkwardness is the entire problem this chapter removes.
The hand-it-back function compiles and runs, but no Rust programmer writes code like this. It exists in this section for one reason: to make the pain concrete before the real tool arrives. The very next section replaces the whole (usize, String) dance with a single character, a &, and the absurdity disappears.
A reference: access without ownership
The fix for the last section’s absurdity is a small one, and you have seen its shape before. Instead of handing the whole value to a function and waiting to get it back, you hand the function a reference: a way to reach a value that someone else owns. The Rust Book puts it plainly. A reference is an address you can follow to get at the data, but the data still belongs to another variable. You write one with an &, so &s1 means a reference to s1. The act of making one has its own verb: you are borrowing.
If you write Go, you have done exactly this every time you passed a *T instead of a T to avoid copying a struct. The mechanics rhyme: &s1 in Rust is the same gesture as &s1 in Go, a pointer to a value the callee can read without taking a copy. What Rust adds is the word owner and the relationship behind it. The pointer does not just point; it points at a value that s1 still owns, and the borrow is understood to be temporary. s1 lends the value out and stays the owner the whole time.
Here is the canonical read-through-a-reference example. calculate_length takes a &String, a shared reference to a String, reads its length, and returns just that usize. The caller passes &s1, gets the number back, and then keeps using s1 on the next line. Run it:
// A shared reference (&String) lets a function read a value it does not own.
// The caller lends s1 out, the function reads it, and s1 is still the owner.
fn main() {
let s1 = String::from("hello");
// &s1 borrows s1 for reading. calculate_length never owns it, so there is
// nothing to hand back: s1 is still usable on the very next line.
let len = calculate_length(&s1);
println!("the length of '{s1}' is {len}");
println!("owner still has: {s1}");
}
// The parameter type &String means "a shared reference to a String": look, do
// not touch. The function reads the length and returns just that number.
fn calculate_length(s: &String) -> usize {
s.len()
}Trace where ownership lives. s1 owns the String for the whole function. When you call calculate_length(&s1), you do not move the String into the function; you lend it a reference. The function reads s.len() through that reference and returns a plain number, which is why both final lines can still print s1. Compare this with the move from the last section, where s1 would have been spent the instant you passed it. Nothing was spent here. The borrow appeared, the function used it, the borrow ended, and the owner never noticed it was gone.
s1 still owns the String; a second, non-owning pointer &s1 just appeared alongside it to read, and it will disappear when the borrow ends.This is the direct payoff for the previous section. Passing a value to read it no longer costs you the value. You do not have to invent a return-it-back dance, because there is nothing to return: the function never owned the String, so it has nothing to give up. A &String is a borrow that says look, and the next sections show its partner, the borrow that says change it, and then the single rule that governs both.
A reference lets you access a value without owning it, and creating one is called borrowing. Because you never owned the value, you never have to give it back: the owner stays the owner, and the value was theirs the whole time. You borrowed a look at it, not the thing itself.
Look, do not touch
The first kind of borrow is the cheap one. A shared reference, written &T, lets you look at a value you do not own. The promise it makes is narrow and exact: you may read, and you may not write. That is the whole of &T, and it is why this section is titled the way it is. A shared reference is a pass to look, not a pass to touch.
Because reading never disturbs anyone, you can hand out as many shared references to the same value as you like, all alive at once. This is the opposite of the move from the previous section, where handing the value to one place took it away from everywhere else. A shared borrow takes nothing away: the owner keeps owning, and ten readers can hold &greeting at the same moment without stepping on each other. The program below does exactly that. Two shared references, r1 and r2, point at the same String and both read it; a function takes a third &String just to ask its length. All of them coexist, because none of them changes a thing. Run it:
// A shared reference &T is "look, do not touch": you can hand out
// any number of them at once, and none of them may change the value.
fn read_length(s: &String) -> usize {
// A shared reference can read, so .len() is fine here.
s.len()
}
fn main() {
let greeting = String::from("hello");
// Two shared references to the same String, alive at the same time.
// Many readers at once is allowed, because readers never interfere.
let r1 = &greeting;
let r2 = &greeting;
println!("r1 sees {r1}, r2 sees {r2}");
// A shared reference may be passed anywhere a read is wanted.
println!("length via shared read: {}", read_length(&greeting));
// Uncomment the next line to see the compiler enforce "do not touch":
// error[E0596]: cannot borrow `*r1` as mutable, as it is behind a `&` reference
// r1.push_str(", world");
}Now look at the line that is commented out at the bottom. It tries to grow the string through r1 with r1.push_str(", world"), and if you uncomment it the program stops compiling. The compiler says error[E0596]: cannot borrow *r1 as mutable, as it is behind a & reference. That is the rule being enforced for you: a shared reference is read-only, so reaching through it to mutate is rejected before the program ever runs. You did not have to remember the rule or wire up a check. The type &String already encoded look-do-not-touch, and the compiler held you to it.
If you write Go, the closest familiar thing is passing a pointer you have agreed, by convention, only to read from. The agreement in Go lives in your head and in code review; nothing stops a function from writing through a *T it was only supposed to inspect. Here the same intent is a different type. A &String is not a writable handle that you have promised to be gentle with; it is a handle that cannot write, full stop, and the compiler is the one keeping the promise. To actually change the value you need the other kind of reference, which is the next section.
Hold on to two facts from this section, because the next two build directly on them. First, shared means many: any number of &T readers can be alive at the same time. Second, shared means read-only: not one of them may change the value. Many readers, no writers. That pairing is half of the single rule this chapter is circling toward.
A shared reference &Tis look, do not touch. You can hand out any number of shared references to the same value at once, because readers never interfere with each other: ten people reading the same page do not get in each other’s way. The one thing none of them may do is change the value.
When rustc talks about &T it calls it an immutable reference, and &mut T a mutable one. This book says shared and exclusive instead, because those words name what actually matters: how many can exist at once. They are the same two things under two sets of labels, so when an error message says borrowed as immutable, read it as borrowed as shared and carry on.
An exclusive reference: borrow to change
The shared reference from the last section is a promise to look and not touch, which is exactly why you can hand out as many as you like. But plenty of work is not reading, it is changing: appending to a buffer, bumping a counter, fixing a field. For that Rust has the other kind of borrow, the exclusive reference, written &mut T. Where &T says look, &mut T says change. And where you can have many shared readers at once, you can have exactly one exclusive writer, with no other reference to that value alive at the same time. One writer, alone.
Here is the canonical example. The owner is a String, declared mut; it lends out a single &mut to a function that pushes onto it, and the appended text is visible back in the owner afterward. Run it:
// change takes an exclusive reference (&mut String): the one borrow allowed
// to write. main lends out exactly one, the callee pushes onto it, and the
// owner sees the appended text after the borrow ends.
fn change(text: &mut String) {
// We may write through this reference because it is exclusive: while it is
// alive, no other reference to text exists.
text.push_str("!!!");
}
fn main() {
// The owner must itself be `mut` to be able to lend out an exclusive borrow.
let mut s = String::from("we did it");
println!("before: {s}");
// The &mut is spelled out right here at the call site. Rust never upgrades a
// shared borrow to an exclusive one behind your back.
change(&mut s);
// The borrow ended when change returned; s is still the owner, now changed.
println!("after: {s}");
}Two details carry the whole idea. First, change writes through the reference, text.push_str("!!!"), and you are allowed to because &mut String is the exclusive kind: while it is alive, nothing else can be looking at that string, so there is no one to surprise. Second, the change reaches the owner. The exclusive reference pointed straight at s’s value, so pushing through it is pushing onto s itself; when change returns and the borrow ends, s is still the owner, now holding we did it!!!. The owner lent the value out, the borrower changed it in place, and the owner got it back, changed.
This is the same favor you do in Go when you pass a *T instead of a T so the callee can mutate what you point at. The difference is the guarantee that comes with it. In Go, two goroutines can each hold a pointer to the same value and both write to it, and nothing stops them until the race detector happens to catch it at runtime. In Rust, an exclusive reference is genuinely exclusive: the compiler will not let a second reference to that value exist while it is alive, shared or exclusive. The right to mutate and the absence of any other view of the value come together, by construction.
Notice also what is loud and what is quiet. To lend out an exclusive borrow at all, the owner must be declared let mut s, and the borrow is spelled out at the call as change(&mut s). You can read the right to mutate directly off the call site; it is never inferred and never silent. Hold this next to section 03: shared is many readers and no change, exclusive is one writer and change, and the two never overlap. That single contrast is the rule the next section turns into the reason data races cannot happen.
&data) at once, or by exactly one exclusive writer (&mut data), but never both at the same time. The two sides never light up together: that is shared-XOR-mutable.&mut T is an exclusive reference: the one borrow allowed to change the value. While it is alive, no other reference to that value may exist, not even a shared reader. The rule is symmetric and short: exactly one writer, or none. That is the other half of the picture from section 03, where you could have any number of &T readers precisely because none of them could change anything.
You can lend out an exclusive reference only if the owner itself is declared let mut, and the borrow is written out at the call: change(&mut s). Rust never silently upgrades a shared borrow into an exclusive one, so the right to mutate is always visible in the code. If you see no &mut at a call, that call cannot change your value, full stop.
The one rule, and why the compiler enforces it
If you came from Go, sit with how different this feels. In Go, two goroutines each incrementing one shared counter is a data race, and the program compiles, ships, and runs without a word of complaint. It just quietly gets the answer wrong: eight goroutines that should drive the count to 800000 leave it at something like 318256 instead, because increments from different goroutines land on top of each other and a write is lost. Nothing crashes, nothing logs, the number is simply too small, and the only way you find out is to run the program under go run -race on a code path your tests happen to exercise. Here the same shape, a reader and a writer touching one value at once, is not a runtime hazard you hunt for later. It is a compile error you cannot get past. The mistake is moved from production to the keystroke, which is the entire point.
The program below is the corrected version: the shared readers r1 and r2 finish at the println! before a single &mut takes over, so it compiles and runs. Below it, the figure shows the version that does not compile, where a reader and a writer overlap in time. Run the good one:
fn main() {
// Many shared readers can be alive at the same time: each &s is
// "look, do not touch", and several lookers never conflict.
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1} and {r2}"); // last use of the shared readers; both borrows end here
// Now that no shared reference is still alive, a single exclusive
// &mut is allowed: one writer, alone.
let r3 = &mut s;
r3.push_str(", world");
println!("{r3}");
}&s) and the exclusive borrow (&mut s) overlap in time, and that overlap is the one thing the rule forbids. The owner s is present the whole time underneath; this red stretch is exactly what E0502 refuses to compile.At any moment a value has either any number of shared readers or a single exclusive writer, never a mix of the two. You do not have to hold this in your head as a law yet. You just watch the compiler refuse the mixed case, every time, and trust that the refusal is the rule. Part III states it formally; here, the borrow checker states it for you.
The E0502 error fires only because r1 is read again, in the final println!, after the &mutis taken. If the shared reference’s last use came before the writer, its borrow would already be over and the code would compile. The conflict is about two borrows overlapping in time, not about which lines sit above which in the file. Reorder so the reader finishes first and the very same lines build.
Borrows are temporary; the owner outlives them
The last section ended with a refusal: a shared reader was still in use when a writer tried to step in, so the compiler rejected the whole program with E0502. That refusal can feel like the rule is about where the references appear, as if a & anywhere in a block poisons it for a &mut until the block closes. It is not. The rule is about when a reference is actually used, and the fix is two lines moved, not a single annotation added.
Here is the same scenario from section 05, reordered so the readers finish before the writer begins. The two shared references r1 and r2 are created, read, and printed; that println! is the last place either one is used. Only after it does a lone &mut take over and push onto the string. Run it:
// The same scenario as the conflict, with two lines reordered. The shared
// readers finish reading before the exclusive writer ever appears, so no
// shared reference is alive when the &mut is taken. It compiles and runs.
fn main() {
let mut s = String::from("hello");
let r1 = &s; // a shared reader
let r2 = &s; // another shared reader: many readers at once is fine
println!("{r1} and {r2}"); // last use of the readers: their borrows end here
let w = &mut s; // fine now: no shared reference is still alive
w.push_str(", world"); // the one exclusive writer changes the owner's String
println!("{w}");
}The point is what the compiler counts as the life of a borrow. A borrow is not pinned to the block it sits in; it lasts only from where the reference is created to the last place it is used, and not one line further. By the time w takes its exclusive borrow, the last use of r1 and r2 is already behind us, so no shared reference is alive to conflict with it. Many readers coexisted, finished, and stepped aside; then exactly one writer took the floor. Shared-XOR-mutable held the entire time, because the shared part and the mutable part never actually overlapped. Same code as the broken version, two lines reordered, and the conflict is simply gone.
s spans the whole timeline. The shared reads end cleanly before the exclusive write begins, so the two borrows never overlap; the owner is there before, during, and after them all.Through all of it, one thing never moved: the owner. s was declared before the first borrow, it lent out shared access and then exclusive access, and it is still s at the end, holding the changed string. The readers and the writer were temporary windows; the owner spanned them all and resumed full control the instant each borrow ended. This is the answer to the worry that opened the chapter. Lending a value out does not give it away. You looked, you changed, and the owner kept owning the whole time.
If you write Go, the contrast is in who tracks this and when. Go has no notion of a borrow ending: a pointer is just a pointer, alive as long as something holds it, and whether two goroutines touch it at a bad moment is discovered, if at all, by the race detector at runtime, on the code paths your tests happen to exercise. Rust works out the last use of every reference at compile time and refuses the program before it runs if a write would overlap a read. You pay for that with up-front strictness; you get back a whole category of timing bug that cannot reach production. The machinery that does this bookkeeping, and the words for talking about how long a reference may live, is the borrow checker, and that is exactly where Part III begins.
A borrow is not pinned to a whole block; it lasts only from where the reference is created to the last place it is used. Once the readers are done reading, their borrows are over, and a writer can step in. The owner was there before, during, and after.
The same rule, scaled up to threads
Here is where the rule from the last few sections collects its real prize. Define a data race plainly: two or more parts of a program touch the same value at the same time, at least one of them is writing, and nothing coordinates the order. Read that definition slowly and you will see it is the forbidden shape wearing a different hat. Two-or-more accessors plus at least one writer is exactly shared-and-mutable, the one combination the borrow rule already refuses. Many readers or one writer, never both, is not just a tidy single-thread invariant. It is the precise condition that a data race needs in order to exist, removed at the root.
The interesting part is that nothing new has to be invented for threads. The compiler does not run one set of checks for code on a single thread and a different set for code spread across many. It is the same shared-XOR-mutable rule the whole way down, so a sharing mistake that would be a borrow error in a loop becomes a compile error when you hand the value to another thread. In Go, by contrast, two goroutines writing the same map is perfectly compilable; you find the race at runtime if your tests happen to exercise it, or in production if they do not. Go ships a race detector for exactly this reason, and it is a good tool, but it is dynamic: it only sees races on the code paths that actually run. Rust rejects the whole category before the program starts.
You can watch this happen at teaser altitude with two near-identical programs. To share one value with a spawned thread, you wrap it in Arc (atomically reference-counted), make a second handle with Arc::clone, and move that handle into the thread. The version below compiles and prints from both the spawned thread and main. Now read the comment: swap Arc for Rc (the plain, single-thread reference count from earlier in this part) and the program stops compiling, with error[E0277]: `Rc<i32>` cannot be sent between threads safely. You did not change the logic; you changed the type to one the compiler knows is not safe to share across threads, and it refused before a single line ran. Run it:
// Share one value with a spawned thread. Arc is the version of shared
// ownership the compiler will let you send across a thread boundary.
//
// Swap Arc for Rc (use std::rc::Rc; Rc::new; Rc::clone) and this stops
// compiling: error[E0277]: `Rc<i32>` cannot be sent between threads safely,
// with the help line: the trait `Send` is not implemented for `Rc<i32>`.
// The single-thread borrow rule reappears here as a Send type error,
// caught before the program ever runs.
use std::sync::Arc;
use std::thread;
fn main() {
let counter = Arc::new(5); // one shared owner
let from_thread = Arc::clone(&counter); // a second handle to the same value
let handle = thread::spawn(move || {
println!("thread sees: {}", from_thread);
});
handle.join().unwrap(); // wait for the thread to finish
println!("main sees: {}", counter);
}That one-line phrase, safe to send between threads, is the whole of the marker you need to recognize for now. Send means a value can be handed to another thread; Rc lacks it because its reference count is not built for two threads to bump at once, and Arc has it because its count is atomic. That is the entire teaser: a sharing rule that would be a borrow error on one thread surfaces here as an ordinary type error you can see and fix. The machinery (what Send is, how it is derived, when you need Mutex alongside Arc) is the concurrency chapter in Part VIII; here the point is only the feeling that the compiler stops the unsafe sharing for you.
Be precise about what that buys you, because the slogan oversells if you let it. Fearless concurrency means no data races, caught at compile time, and nothing more. It does not mean no deadlocks, where two threads wait on each other forever, and it does not mean no logic races, where the program is memory-safe but computes the wrong answer because two correct steps ran in an unlucky order. Those are still possible in Rust and still your problem to think through. What the borrow rule removes is one specific, vicious, and famously hard-to-debug category: the memory-corrupting data race. It removes that one completely, which is a great deal, but it is one category, not all of them.
A data race needs three things at once: two or more accessors touching the same value, at least one of them writing, and no coordination between them. Line that up against the borrow rule and it is the same picture: shared-and-mutable, the exact combination of &T and &mut T that cannot coexist. Forbid that overlap everywhere, on one thread and across many, and a data race simply has no shape left to take. It cannot compile.
Fearless concurrency is a precise claim, not a blanket one. It means no data races, caught by the compiler before the program runs. It does not mean no deadlocks and no logic races; a Rust program can still hang waiting on itself, or produce a wrong answer because two correct operations interleaved badly. Rust eliminates one whole category of concurrency bug, the data race, and leaves the rest to you. Read it as one fewer class of disaster, not zero concurrency bugs.
What you can do now, and what comes next
Step back and look at the whole picture you have built. An owner holds a value and keeps holding it. A shared reference (&T) is the look borrow: read-only, and you can have many at once, because many readers stepping on nothing is harmless. An exclusive reference (&mut T) is the change borrow: it can write, so you get exactly one and no other reference may be alive beside it. Every borrow is temporary, scoped to its last use, and the owner outlives all of them and resumes full control the moment they end. That is the entire mental model, and you can now reason about a piece of Rust code by asking four small questions: who owns this, who is reading it, who is writing it, and when does each borrow end.
One rule sits underneath all of it: many readers or one writer, never both. That is not an arbitrary restriction; it is the exact shape that makes a data race impossible. A data race needs two accessors touching the same value at the same time with at least one of them writing, and this rule forbids precisely that overlap. The borrow checker enforces it in ordinary single-threaded code, the kind you saw the compiler reject with E0502 a few sections ago, and the same rule scales up to threads untouched. This is the intuition behind the phrase fearless concurrency, and it is a real and narrow claim: Rust statically rules out data races, not every concurrency bug. Deadlocks and logic races are still yours to get wrong. Part VIII cashes this out properly. For now, hold the shape: the rule that organizes a single function is the same rule that makes threads safe.
Here is the honest part. You can reason about borrowing in pictures, but you cannot yet satisfy the borrow checker from memory, and you are not meant to. This chapter taught you zero borrow-checker syntax on purpose. You have seen the compiler reject a bad borrow and accept a good one, and you can predict which is which by drawing the timeline, but you have not learned the formal rules that produce those verdicts, nor the notation Rust uses to talk about how long a reference stays valid. That formal account is the whole job of Part III, where the borrow checker and lifetimes get stated as rules you can apply deliberately rather than feel your way through.
&T) may look at once; exactly one exclusive writer (&mut T) may change, alone. The strip below shows each borrow as a short window inside the owner’s lifetime.To see exactly where Part III picks up, try one thing the pictures in this chapter cannot yet cover: write a function that creates a value and tries to return a reference to it. The value is owned by the function, so it is dropped when the function returns, which would leave the returned reference pointing at nothing. Rust refuses to compile this with error[E0106]: missing lifetime specifier, telling you the return type contains a borrowed value with no source for the borrow. Read that error as the compiler pointing straight at the one thing this chapter deliberately left out: it is asking you for a lifetime, a piece of notation you will learn to supply in Part III. At this altitude the fix is simpler than the question. Do not return a borrow of a local; return the owned value itself, move it out, and the owner problem disappears. If you wrote Go, this is the moment the languages visibly part: Go’s runtime would quietly keep that value alive on the heap and hand back the pointer, while Rust stops at compile time and makes the ownership of the result explicit. Same instinct, different bill, paid up front instead of at runtime.
You can already reason about borrowing in pictures: who owns the value, who is reading it through a shared &T, who is writing it through the one exclusive &mut T, and when each borrow ends. What you cannot yet do is fight the borrow checker from memory, and that is by design. This chapter taught no borrow-checker syntax. Part III turns these pictures into the formal rules, so the verdicts you can currently feel become verdicts you can derive.
Write a function that builds a value and tries to return a reference to it, and the compiler stops you with error[E0106]: missing lifetime specifier: the return type contains a borrowed value, but there is no value for it to be borrowed from. That error is the compiler pointing at the exact thing this chapter left out, asking you for a lifetime. You will learn to answer it in Part III. For now the fix is to return the owned value instead of a borrow of a local, so there is nothing left dangling.
The whole chapter in seven lines
- A reference lets you access a value without owning it; borrowing is creating one, and the owner stays the owner.
- A shared reference (
&T) is look, do not touch, and you can have many at once. - An exclusive reference (
&mut T) is the one borrow allowed to change the value, and while it lives no other reference may. - The one rule is many readers or one writer, never both; the compiler enforces it, as you saw with
E0502. - Borrows are temporary, ending at their last use, so the owner outlives every borrow and gets full control back.
- That same shared-XOR-mutable shape is why data races cannot compile, the intuition behind fearless concurrency, cashed in Part VIII.
- You can reason in pictures now; the formal borrow checker and lifetimes are Part III.