Rust (2024 edition) Module 3 – Ownership, borrowing and slices
References and the borrowing rules
Borrow Rust values with shared and mutable references, follow the rule of many readers or one writer, and see why a borrow ends at its last use.
What you will learn
- Use shared and mutable references to lend a value, and say what each one allows
- Apply the rule of many shared references or one mutable reference to predict E0502 and E0499
- Fix a borrow error by ending the borrow, at its last use, before the value changes
- Explain what a two-phase borrow is, and predict which method calls it allows
Before you start
What you’ll learn · Before you start
What you will learn
- Use shared and mutable references to lend a value, and say what each one allows
- Apply the rule of many shared references or one mutable reference to predict E0502 and E0499
- Fix a borrow error by ending the borrow, at its last use, before the value changes
- Explain what a two-phase borrow is, and predict which method calls it allows
Before you start
On this page
let first = &queue[0]; followed by queue.push(50); is two lines that Rust refuses with error E0502. The push needs
a fifth place in a Vec that has room for four, so it may move all four items to a new block of memory, as a run
below shows, and first would then point into the old block, which has just been freed; in C++, which accepts the
same lines, the standard calls such a reference invalidated.
This lesson shows how references let code use a value without owning it, the two borrowing rules that every reference follows (any number of readers or one writer, and no reference to a value that is gone), and why the order of your lines decides what compiles. The Rust programs here ran on Rust 1.99.0.
A reference lets code use a value without owning it
A reference: A value that refers to another value without owning it: &T lets code read the value and &mut T lets it change it. The compiler checks that no reference outlives the value it refers to. is an address that comes with a promise: &marks refers to marks without
taking it, and the compiler guarantees that it refers to a valid value for as long as it is used. Passing &marks to a
function lends the value, and when the function returns the loan is over. A
shared reference: A reference written &T. Any number of them may refer to a value at the same time, and none of them can change it., &T, lets the borrower read; a
mutable reference: A reference written &mut T, through which code can change the value it refers to. While it is in use, no other reference to that value may be used., &mut T, lets it change the value in place, and only an owner
declared with mut can lend one:
// A shared borrow to read, a mutable borrow to change: main owns the marks throughout.
fn total(marks: &[u32]) -> u32 {
marks.iter().sum()
}
fn add_bonus(marks: &mut Vec<u32>, bonus: u32) {
for mark in marks.iter_mut() {
*mark += bonus;
}
marks.push(bonus);
}
fn main() {
let mut marks = vec![72, 45, 88];
println!("total {}", total(&marks));
add_bonus(&mut marks, 5);
println!("total {}", total(&marks));
let first = &marks[0];
let second = &marks[1];
println!("first {first}, second {second}, both read at once");
let last = marks.last_mut().unwrap();
*last = 10;
println!("marks {marks:?}, total {}", total(&marks));
} Output
total 205 total 225 first 77, second 50, both read at once marks [77, 50, 93, 10], total 230
Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O borrow.rs && ./borrow
Step through the loans in order:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- main owns the marks.
let mut marks = vec![72, 45, 88]makesmainthe owner of the marks, as in the lessons before. - A loan to read.
total(&marks)lends the marks through a shared reference.totalcan read them but not change them, and it adds them up: 205. - The loan ends. When
totalreturns, its reference is gone, andmainhas its marks back to itself. Nothing was moved or copied but an address. - A loan to change.
add_bonus(&mut marks, 5)lends them through a mutable reference: the function adds 5 to each mark and pushes a fifth value, 5, onto the Vec. - Changed in place. The change happened to the Vec that
mainowns, so whenadd_bonusreturns, the next total is 225. - Two readers at once.
firstandsecondare two shared references into the marks, both in use in oneprintln!. Any number of readers may share a value. - One writer.
marks.last_mut()gives one mutable reference to the last mark, and*last = 10changes it through that reference. The marks are 77, 50, 93 and 10, total 230.
main owned the marks the whole time. total read them through &[u32], a borrowed view of the items, and
add_bonus changed them through &mut Vec<u32>, which can also push. Method calls see through a reference, so
marks.iter() reads a Vec and a reference to one alike, while *mark += bonus writes through a reference with an
explicit *.
Checkpoint
Check: Lending a value with & and &mut
The borrowing rules: any number of readers, or one writer
Every borrow: To take a reference to a value, so that other code can use the value while its owner keeps it. A borrow lasts from where the reference is made to the last place it is used. follows two borrowing rules. The Rust book states the first one this way:
At any given time, you can have either one mutable reference or any number of immutable references.
The second is that references must always be valid. The borrow checker: The part of the Rust compiler that checks every reference against the ownership and borrowing rules, and refuses to build a program in which a reference could outlive its value or break those rules. applies both borrowing rules before the program is built, so the program from the opening never runs:
// A reference into a Vec, held across a push that needs more room than the Vec has.
fn main() {
let mut queue: Vec<u64> = Vec::with_capacity(4);
queue.extend([10, 20, 30, 40]);
let first = &queue[0];
queue.push(50);
println!("first is {first}");
} Output (exit status 1)
Printed as an error (standard error)
error[E0502]: cannot borrow `queue` as mutable because it is also borrowed as immutable
--> push_conflict.rs:6:5
|
5 | let first = &queue[0];
| ----- immutable borrow occurs here
6 | queue.push(50);
| ^^^^^^^^^^^^^^ mutable borrow occurs here
7 | println!("first is {first}");
| ----- immutable borrow later used here
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0502`.
Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O push_conflict.rs && ./push_conflict
E0502 names the two borrows that overlap: first, made on line 5 and used on line 7, and the mutable borrow that
push needs on line 6. The reason for the rule shows when the same push runs with no reference held:
// Where the Vec keeps its items before and after a push that needs more room.
fn main() {
let mut queue: Vec<u64> = Vec::with_capacity(4);
queue.extend([10, 20, 30, 40]);
let neighbour: Vec<u64> = vec![0; 4];
let before = queue.as_ptr();
println!("length {}, capacity {}", queue.len(), queue.capacity());
queue.push(50);
let after = queue.as_ptr();
println!("length {}, capacity {}", queue.len(), queue.capacity());
println!("items moved to a new block: {}", before != after);
println!("{} items beside it", neighbour.len());
} Output
length 4, capacity 4 length 5, capacity 8 items moved to a new block: true 4 items beside it
Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O buffer_moves.rs && ./buffer_moves
The capacity went from 4 to 8, and the items moved to a new block; the standard library documents that when a
vector’s length exceeds its capacity, its elements have to be reallocated. Step through what first would have
pointed at:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- A full Vec.
queueholds 10, 20, 30 and 40 in heap block A, which has room for exactly 4 items: capacity 4, length 4. - A reference to item 0.
let first = &queue[0];is a shared borrow:firstholds the address of the first item, inside block A. - A push needs room.
queue.push(50)needs a fifth place. The Vec gets block B, with room for 8 items, copies the four items over, adds 50 and frees block A. buffer_moves.rs ran this push: capacity 8, and the items in a new block. - first would dangle.
firststill holds the old address, inside block A, which has been freed. Reading it on line 7 would read memory the program no longer owns. - rustc refuses. None of that ever runs.
pushneeds a mutable borrow ofqueuewhile the shared borrow infirstis still to be used, which the rules forbid, so rustc reports E0502 and builds nothing. - The fix. In last_use.rs,
firstis printed before the push. Its borrow ends at that last use, so the push may move the items: no reference is left to dangle.
Two mutable references to one value, both in use at once, break the same rule from the other side, and rustc reports
them as error E0499. A narrow exception, two-phase borrows, lets a method call such as v.push(v.len()) read the
vector in its own argument.
Key idea
While a shared reference into a value is still to be used, nothing may change the value, and while a mutable reference is in use, nothing else may touch it: many readers or one writer, checked before the program runs.
Checkpoint
Check: Many readers or one writer
A borrow lasts until its last use, not until the end of the block
A borrow lasts from the line that makes the reference to the last line that uses it. So moving one line can fix E0502:
// A borrow lasts until its last use, so the order of the lines decides what compiles.
fn main() {
let mut queue: Vec<u64> = Vec::with_capacity(4);
queue.extend([10, 20, 30, 40]);
let first = &queue[0];
println!("first is {first}"); // the last use of first: the borrow ends here
queue.push(50); // so the Vec may change now
let copied = queue[0]; // a u64 is Copy: copied holds its own value, no borrow
queue.push(60);
println!("copied is {copied}, queue is {queue:?}");
} Output
first is 10 copied is 10, queue is [10, 20, 30, 40, 50, 60]
Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O last_use.rs && ./last_use
Printing first before the push ends its borrow, so the push is allowed. Copying a value out of the Vec avoids the
borrow altogether: a u64 is Copy, so copied holds its own 10 and points into nothing. Step through which borrow is
in use at each line:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- A borrow starts.
firstborrows the first item ofqueue. From here until its last use, nothing may changequeue. - Its last use. The
println!readsfirstfor the last time and printsfirst is 10. The borrow ends here, not at the end ofmain. - The push is allowed.
queue.push(50)borrowsqueuemutably. No shared borrow is in use any more, so rustc allows it, even if the items move to a new block. - A copy, not a borrow.
let copied = queue[0];copies theu64out of the Vec.copiedowns its own 10 and borrows nothing. - The second push.
queue.push(60)is a mutable borrow again, and it is allowed:copieddoes not point into the Vec, so the Vec may grow or move. - Read at the end. The last
println!readscopiedand the wholequeue, which now holds 10, 20, 30, 40, 50 and 60.
This is called non-lexical lifetimes: the length of a borrow follows the flow of the code, not the braces of the block. It arrived with the 2018 edition, and Rust 1.63 removed the older borrow checker from every edition.
Checkpoint
Check: Where a borrow ends
Two writers, a missing mut and two-phase borrows, reproduced Pro: opens with a Pro, Premium or Ultimate pass
Three more cases come from the same rules. This part reproduces E0499 and E0596 with their fixes, and shows why
v.push(v.len()) compiles while the same call written out by hand does not.
In this part:
- Two mutable borrows of one Vec at once: E0499
- Changing a value needs a binding declared with mut: E0596
- Why v.push(v.len()) compiles: two-phase borrows
- A &mut passed to a function is lent on, not given away
- The errors of this lesson in one table
6 runnable examples (Rust) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
What the rules prevent: invalidated references and data races Pro: opens with a Pro, Premium or Ultimate pass
The two rules stop two bugs that C++ compiles without a word: a reference into a vector that a push has moved, and two threads that write one value at once. This part shows Rust refusing each, with the fix for each, then gives a stock program to repair without cloning a collection.
In this part:
- Pushing onto a Vec while a loop reads it
- Why such a bug hides in tests: which pushes can move the items
- Two threads writing one Vec, refused at compile time
- Exercise: fix a stock program without cloning a collection
7 runnable examples (Rust) · about 6 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise: references and the borrowing rules
These questions are about lending with & and &mut, the rule of many readers or one writer, and where a borrow
ends. Every choice says why it is right or what it misses.
Practice that adapts to you
Practise this lesson
9 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): what is the difference between &T and &mut T?
&T is a shared reference: it lends a value to read, any number of them can be in use at once, and none of them can
change it, so a &T is Copy. &mut T is a mutable reference: it lends the value to be changed in place, needs an
owner declared with mut, and while it is in use no other reference to the value may be used, so it is not Copy.
Both are borrows: the owner keeps the value, and the compiler checks that no reference outlives it. A good answer adds
why: many readers or one writer is what lets the compiler rule out data races and references into moved memory.
-
Fresher to mid
Why does Rust refuse
let first = &v[0]; v.push(4); println!("{first}");when the same three lines compile in C++? What could go wrong at run time, and how would you fix the Rust code? -
Mid
A game keeps
players: Vec<Player>and must move points from player i to player j in one step.let a = &mut players[i]; let b = &mut players[j];is refused. What are your options, and what does each do when i == j? -
Mid to senior
Explain how the compile-time rule of many readers or one writer rules out data races between threads. Which concurrency bugs can a safe Rust program still have?
Key takeaways
- A reference lends a value without moving it,
&Tto read and&mut Tto change it, and the owner keeps the value throughout. - At any moment a value may have any number of shared references or one mutable reference, never both, and rustc refuses a program that breaks the rule (E0502).
- A push may move a
Vec’s items to a new block, which is why a reference into aVecand a change to it cannot overlap. - A borrow lasts until the reference’s last use, so moving a line or copying a value out often fixes a borrow error.
Terms in this lesson
- borrow
- To take a reference to a value, so that other code can use the value while its owner keeps it. A borrow lasts from where the reference is made to the last place it is used.
- borrow checker
- The part of the Rust compiler that checks every reference against the ownership and borrowing rules, and refuses to build a program in which a reference could outlive its value or break those rules.
- mutable reference
- A reference written &mut T, through which code can change the value it refers to. While it is in use, no other reference to that value may be used.
- reference
- A value that refers to another value without owning it: &T lets code read the value and &mut T lets it change it. The compiler checks that no reference outlives the value it refers to.
- shared reference
- A reference written &T. Any number of them may refer to a value at the same time, and none of them can change it.
References
- The Rust Programming Language: References and Borrowing (The Rust Project)
- The Rust Reference: Pointer types (The Rust Project)
- The Rust Standard Library: Vec (Capacity and reallocation) (The Rust Project)
- Rust error codes index: E0499 (The Rust Project)
- Rust error codes index: E0502 (The Rust Project)
- Rust error codes index: E0596 (The Rust Project)
- Announcing Rust 1.63.0: non-lexical lifetimes migration complete (The Rust Project)
- RFC 2094: Non-lexical lifetimes (The Rust Project)
- Rust Compiler Development Guide: Two-phase borrows (The Rust Project)
- The Rustonomicon: Splitting Borrows (The Rust Project)
- Rust error codes index: E0594 (The Rust Project)
- The Rust Standard Library: slice::split_at_mut (The Rust Project)
- The Rust Standard Library: slice::get_disjoint_mut (The Rust Project)
- Announcing Rust 1.86.0 (get_disjoint_mut) (The Rust Project)
- The Rust Standard Library: std::thread::scope (The Rust Project)
- C++ working draft: Modifiers of vector [vector.modifiers] (ISO/IEC JTC1/SC22/WG21)
- C++ working draft: Iterator requirements, general [iterator.requirements.general] (ISO/IEC JTC1/SC22/WG21)
Report a problem with this lesson
Kept only in this browser. Your Learn progress