Borrowing and References
Using a value without taking it: shared and mutable references, the rule of many readers or one writer, how the borrow checker enforces it, and why Rust has no dangling references.
Borrowing instead of moving
On Ownership, passing a
String or Vec to a function moved it, and
the caller lost it. Most functions only need to look at a value, or
change it in place, and then let the caller carry on.
Borrowing is using a value through
a reference, created with &, without taking
ownership of it: the owner stays the owner, and the value is not
dropped when the borrower is done.
fn total_kwh(readings: &Vec<u32>) -> u32 {
let mut total = 0;
for kwh in readings {
total += kwh;
}
total
}
fn main() {
let readings = vec![350, 412, 120];
let total = total_kwh(&readings); // lend readings to the function
println!("total {total} kWh over {} readings", readings.len());
}
total 882 kWh over 3 readings
&readings creates a
reference: on a 64-bit machine an 8-byte address of the vector, nothing
more. The type &Vec<u32> in the signature
says "a reference to a vector", so the caller must lend, and the
function cannot keep or free the vector. The & is
visible at the call site, so a reader of main knows
that readings is only lent out.
You rarely have to write * to follow a reference when
reading: method calls (readings.len()) and
println! follow references automatically, and a
for loop over &Vec<u32> hands
out &u32 values that += accepts. A
comparison needs the value itself: *kwh > 400, as in
a loop further down.
Shared references
A
shared reference, written
&T, gives read-only access to a value. Any number
of shared references to the same value may exist at the same time,
and shared references are Copy, so handing one to
several functions is free. Trying to change a value through one is a
compile error:
fn add_reading(readings: &Vec<u32>, kwh: u32) {
readings.push(kwh);
}
fn main() {
let readings = vec![350, 412];
add_reading(&readings, 120);
println!("{readings:?}");
}
example.rs:2:5: error[E0596]: cannot borrow `*readings` as mutable, as it is behind a `&` reference: `readings` is a `&` reference, so it cannot be borrowed as mutable
help: consider changing this to be a mutable reference
|
1 | fn add_reading(readings: &mut Vec<u32>, kwh: u32) {
| +++
Mutable references
A
mutable reference, written
&mut T, gives temporary exclusive access to a
value, including the right to change it. You can only take one from
a variable that is itself declared mut, and the caller
writes &mut too, so a change through a function
call is never a surprise. To assign to the value behind a reference,
follow it with *:
fn add_reading(readings: &mut Vec<u32>, kwh: u32) {
readings.push(kwh);
}
fn bump(counter: &mut u32) {
*counter += 1; // * reaches the value behind the reference
}
fn main() {
let mut readings = vec![350, 412];
let mut rejected = 0;
add_reading(&mut readings, 120);
bump(&mut rejected);
println!("{readings:?}, rejected {rejected}");
}
[350, 412, 120], rejected 1
This is how Rust does what C does with "pass a pointer so the
function can change the caller's variable" (see
Pointers), with the
difference that the compiler knows which functions may change what.
A &mut parameter in a signature is a clear
statement: this function changes its argument.
Many readers or one writer
The central borrowing rule fits on one line. At any moment, a value can have either any number of shared references or exactly one mutable reference, but not both. This rule is often called aliasing XOR mutation: a value can be aliased (reachable through several references) or mutated, never both at once, which rules out a whole family of bugs where code reads data that someone else is changing underneath it.
Two mutable borrows of the same vector at once:
fn main() {
let mut readings = vec![350, 412];
let first = &mut readings;
let second = &mut readings;
first.push(120);
second.push(95);
}
example.rs:4:18: error[E0499]: cannot borrow `readings` as mutable more than once at a time: second mutable borrow occurs here
The more instructive case is a shared borrow of one element while the vector grows. It looks harmless, and in C it is a real bug:
fn main() {
let mut readings = vec![350, 412];
let first = &readings[0];
readings.push(120);
println!("first: {first}");
}
example.rs:4:5: error[E0502]: cannot borrow `readings` as mutable because it is also borrowed as immutable: mutable borrow occurs here
error[E0502]: cannot borrow `readings` as mutable because it is also borrowed as immutable
--> example.rs:4:5
|
3 | let first = &readings[0];
| -------- immutable borrow occurs here
4 | readings.push(120);
| ^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 | println!("first: {first}");
| ----- immutable borrow later used here
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0502`.
Why refuse it? push may find the buffer full, allocate
a bigger one, copy the elements and free the old buffer, exactly
like C's
realloc. first would then point into freed memory: a
use-after-free. In C this compiles and usually works in a quick
test, because the buffer happens to have room; it fails only when
the vector grows. Rust does not try to work out whether this
particular push reallocates. The rule is simpler and
always holds: while a shared reference is in use, nobody changes the
value.
The same rule rejects changing a collection while looping over it, a
bug that Python reports at run time for dictionaries ("dictionary
changed size during iteration") and Java with a
ConcurrentModificationException. Here the compiler's
help line even names the usual fixes:
fn main() {
let mut readings = vec![350, 412, 120];
for kwh in &readings {
if *kwh > 400 {
readings.push(*kwh / 2);
}
}
println!("{readings:?}");
}
example.rs:5:13: error[E0502]: cannot borrow `readings` as mutable because it is also borrowed as immutable: mutable borrow occurs here
= help: consider using an index-based loop instead, or collecting modifications into a separate collection
Across threads, the same rule is what prevents data races: two
threads cannot both hold a &mut to one value, so
shared data must be protected by a lock or similar type (see
Concurrency and
race conditions).
The borrow checker
The borrow checker is the part of the Rust compiler that checks every reference in a program against the ownership and borrowing rules: that no reference is used after its value is gone, and that no value is changed while it is borrowed elsewhere. It works entirely at compile time; the compiled program contains no checks and no reference counts, and a reference costs what a C pointer costs.
A borrow does not last until the end of the block. With
non-lexical lifetimes
(NLL), the borrow checker considers a reference alive only from
where it is created to where it is last used. Move the
println! from the E0502 example above the
push, and the program compiles:
fn main() {
let mut readings = vec![350, 412];
let first = &readings[0];
println!("first: {first}"); // last use of first: the borrow ends here
readings.push(120);
let a = &readings;
let b = &readings; // any number of shared borrows at once
println!("{} readings, {} readings", a.len(), b.len());
}
first: 350 3 readings, 3 readings
The borrow checker is conservative: it rejects every program that breaks the rules, and also some programs that are actually fine but that it cannot prove fine. As of October 2026, a next iteration of the borrow checker (the project known as Polonius) is enabled on the nightly channel, announced in August 2026, to accept more such programs; stable Rust still uses the current one. When it refuses correct-looking code, the fixes on this page usually help, and the error message almost always points at the three places that matter: where the borrow starts, where the conflicting use is, and where the borrow is used later.
No dangling references
A dangling reference is a reference to memory whose value has already been dropped or moved away, the Rust name for C's dangling pointer. The classic way to create one in C is to return the address of a local variable. In Rust it does not compile:
fn latest_label(meter: &str) -> &String {
let label = format!("{meter}: 350 kWh");
&label
}
fn main() {
println!("{}", latest_label("M-30004"));
}
example.rs:3:5: error[E0515]: cannot return reference to local variable `label`: returns a reference to data owned by the current function
label is dropped when the function returns, so a
reference to it would point to freed memory. The fix is to return
the String itself, which moves ownership to the caller:
fn latest_label(meter: &str) -> String {
format!("{meter}: 350 kWh") // return the String itself: ownership moves out
}
fn main() {
println!("{}", latest_label("M-30004"));
}
M-30004: 350 kWh
The same protection works inside a function. A reference stored in a variable that lives longer than the value it points to is refused with E0597, "does not live long enough":
fn main() {
let latest;
{
let reading = String::from("350 kWh");
latest = &reading;
}
println!("{latest}");
}
example.rs:5:18: error[E0597]: `reading` does not live long enough: borrowed value does not live long enough
How the compiler tracks how long each reference may live, and the cases where you have to tell it, is the subject of Lifetimes.
Slices are borrows
A slice is a reference to a run of elements inside something else:
&str is a borrowed piece of UTF-8 text,
&[u32] a borrowed piece of an array or vector.
Slicing copies nothing. A slice reference is a pointer plus a
length, 16 bytes on a 64-bit Linux machine (ARM64), twice the size
of an ordinary reference. Functions that only read text or numbers
should take slices, because then they accept every kind of argument:
fn meter_id(line: &str) -> &str {
match line.find(';') {
Some(end) => &line[..end],
None => line,
}
}
fn total_kwh(readings: &[u32]) -> u32 {
readings.iter().sum()
}
fn main() {
let line = String::from("M-30004;350;fixed");
println!("meter {}", meter_id(&line)); // &String works where &str is expected
println!("literal {}", meter_id("M-30007;412;dynamic"));
let readings = vec![350, 412, 120, 95];
println!("all {}", total_kwh(&readings)); // &Vec<u32> works where &[u32] is expected
println!("first two {}", total_kwh(&readings[..2]));
println!("array {}", total_kwh(&[100, 200]));
println!("sizes: &str {} bytes, &[u32] {} bytes, &u32 {} bytes",
size_of::<&str>(), size_of::<&[u32]>(), size_of::<&u32>());
}
meter M-30004 literal M-30007 all 977 first two 762 array 300 sizes: &str 16 bytes, &[u32] 16 bytes, &u32 8 bytes
meter_id returns a slice of its argument: the first
seven bytes of the same text, not a copy. A slice is a borrow like
any other, so while it is in use, the text it points into cannot
change:
fn meter_id(line: &str) -> &str {
match line.find(';') {
Some(end) => &line[..end],
None => line,
}
}
fn main() {
let mut line = String::from("M-30004;350;fixed");
let id = meter_id(&line);
line.clear(); // reuse the buffer for the next line
println!("meter {id}");
}
example.rs:11:5: error[E0502]: cannot borrow `line` as mutable because it is also borrowed as immutable: mutable borrow occurs here
In C the equivalent, a char * into a buffer that is
then overwritten, prints whatever the next line contains. Copy the
id out with id.to_string() before clearing the line, or
print it first. Slices of strings and the byte-versus-character
question are covered on
Collections and Strings.
Working with the borrow checker
Most borrow errors in everyday code come from holding a reference longer than needed. A handful of patterns solve nearly all of them:
struct Meter {
id: String,
readings: Vec<u32>,
}
fn main() {
let mut readings = vec![350, 412, 120];
// 1. Copy small values out instead of keeping a reference.
let first = readings[0];
readings.push(95);
// 2. Collect the changes first, apply them after the loop.
let mut extra = Vec::new();
for kwh in &readings {
if *kwh > 400 {
extra.push(*kwh / 2);
}
}
readings.extend(extra);
// 3. Borrow different fields of a struct separately.
let mut meter = Meter { id: String::from("M-30004"), readings };
let id = &meter.id;
meter.readings.push(120);
println!("{id}: first {first}, now {:?}", meter.readings);
}
M-30004: first 350, now [350, 412, 120, 95, 206, 120]
-
Copy or index instead of borrowing. A
u32copied out of the vector is independent of it. An index (usize) is not a borrow either, and is checked when used. - Separate reading from changing. First collect what must change, then change it, as in the loop above.
-
Borrow fields, not the whole struct. The compiler
tracks fields separately inside one function, so
&meter.idand a change tometer.readingscan coexist. A method that takes&mut selfborrows the whole struct, so the same split across method calls does not work. -
Own the data when a borrow gets complicated.
Storing a
Stringinstead of a&str, or cloning a small value, often makes a design simpler at a cost you will never measure. - Read the whole error. The three labels (borrow starts, conflicting use, later use) show exactly which lines to reorder.
Sometimes shared, changeable data is what the design really needs, for example a cache that several parts of the program update. Rust has types for that, which move the borrowing check to run time or use locks: see Smart Pointers and Concurrency.