Rust 15 🦀 References and Borrowing Rules
References are how Rust lets you use a value without taking ownership of it. A reference is a pointer to a value that belongs to someone else, and borrowing is the act of creating a reference. The owner retains responsibility for the value; the borrower has temporary access. The compiler verifies that every borrow is valid: the reference does not outlive the value it points to, and it does not conflict with other borrows in ways that would cause aliasing with mutation. This is the mechanism that eliminates data races and dangling pointers without runtime cost.
The borrowing rules are simple to state and strict in enforcement. At any given time, you may have either one mutable reference or any number of immutable references to a value, but not both. A reference cannot outlive the value it points to. These two rules are checked at compile time by the borrow checker, and they are the reason Rust code that compiles is free from data races. The rules feel restrictive at first, but they are what makes safe concurrency possible and what prevents the subtle bugs that plague C and C++.
This chapter covers immutable references, mutable references, the borrowing rules, the relationship between references and ownership, how the borrow checker reasons about lifetimes, the patterns that satisfy the rules, and the common errors that arise when they are violated.
Key point: A reference (&T) borrows a value without taking ownership. At any given time, a value may have either one mutable reference (&mut T) or any number of immutable references (&T), but not both. References must not outlive the value they point to. The borrow checker enforces these rules at compile time.
Why references and borrowing exist
The ownership-transfer problem. Moving a value into a function transfers ownership, which means the caller cannot use the value afterward. For small values that is fine, but for a String, a Vec, or a large struct, moving on every call would be impractical. References let a function use a value without taking it.
The copy-cost problem. Cloning a value before passing it to a function avoids the ownership transfer but duplicates the data. For a large vector, that is expensive. Borrowing is cheaper: a reference is a pointer, and passing it does not copy the value.
The aliasing problem. When two pointers refer to the same memory and one writes, the other sees a change it did not expect. When one frees and the other reads, the result is undefined. Borrowing rules prevent both: no mutable aliasing, no dangling references.
The concurrency problem. A data race occurs when two threads access the same memory and at least one writes. If references obey the borrowing rules, a value can be shared across threads immutably (safe because no thread writes) or moved to one thread, but never shared mutably without synchronization. The compiler verifies this.
The reasoning problem. Borrowing makes the flow of access explicit. A function signature with &T says “I read but do not modify.” A signature with &mut T says “I may modify, and you cannot use the value while I hold the reference.” The caller knows exactly what the function does with the value.
a. Immutable references
An immutable reference is written &T. It allows reading the value but not modifying it.
fn length(s: &String) -> usize {
s.len()
}
fn main() {
let s = String::from("hello");
let n = length(&s); // borrow s
println!("{} has {} characters", s, n); // s is still valid
}
The &s creates a reference to s, and the function takes that reference as a parameter. The function can read the String but cannot modify it or take ownership. When the function returns, the borrow ends, and s remains the owner.
An immutable reference allows multiple simultaneous borrows:
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{} {} {}", r1, r2, r3); // all three are valid
Reading through multiple references is safe because none of them can modify the value. The compiler permits any number of immutable borrows.
The idiomatic parameter type for a function that reads a string is &str, not &String. &str accepts both string literals and &String through deref coercion:
fn length(s: &str) -> usize {
s.len()
}
let s = String::from("hello");
length(&s); // &String coerces to &str
length("hello"); // &str literal
The same principle applies to slices: &[T] is more general than &Vec<T>.
b. Mutable references
A mutable reference is written &mut T. It allows reading and modifying the value.
fn add_exclamation(s: &mut String) {
s.push_str("!");
}
fn main() {
let mut s = String::from("hello");
add_exclamation(&mut s);
println!("{}", s); // hello!
}
The variable must be declared mut to allow a mutable reference. The &mut s creates a mutable reference, and the function takes it. The function can modify the String but cannot take ownership.
A mutable reference requires exclusive access. While it exists, no other reference — mutable or immutable — may exist to the same value:
let mut s = String::from("hello");
let r = &mut s;
// let r2 = &s; // Error: cannot borrow s as immutable
// let r3 = &mut s; // Error: cannot borrow s as mutable more than once
r.push_str("!");
The exclusivity is what prevents data races and aliasing. If two mutable references existed, one could invalidate the other’s view of the data. If a mutable and an immutable reference existed simultaneously, the immutable reference could read a value that was being changed.
c. The borrowing rules
The two rules are:
- At any given time, you can have either one mutable reference or any number of immutable references.
- References must always be valid — they cannot outlive the value they point to.
The first rule is about aliasing. The second is about lifetimes. Together they ensure that every reference is either the only writer or one of many readers, and that no reference outlives its referent.
The first rule is often summarized as “aliasing XOR mutation.” Either you have aliasing (multiple references) with no mutation, or mutation (one reference) with no aliasing.
let mut data = vec![1, 2, 3];
// Multiple readers — OK
let r1 = &data;
let r2 = &data;
println!("{:?} {:?}", r1, r2);
// r1 and r2 go out of scope here
// One writer — OK
let w = &mut data;
w.push(4);
The key is that the readers and the writer do not overlap. Once the readers are no longer used, the writer can take exclusive access.
The second rule is enforced by the borrow checker through lifetime analysis. A reference cannot be returned from a function if the value it points to is local to that function:
// Error: returns a reference to a local value
fn dangling() -> &String {
let s = String::from("hello");
&s
}
The String is dropped at the end of the function, so any reference to it would dangle. The compiler rejects the code.
d. Non-lexical lifetimes
The borrow checker in modern Rust uses non-lexical lifetimes. A borrow ends at the last point where the reference is used, not at the end of the enclosing block.
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
// r1 and r2 are last used here
let r3 = &mut s; // OK: r1 and r2 are no longer used
r3.push_str("!");
println!("{}", r3);
The immutable borrows r1 and r2 end after the println!, so the mutable borrow r3 can begin. In the old lexical model, the borrows would have lasted until the end of the block, and the mutable borrow would have been rejected.
This makes many patterns that were previously awkward work naturally. The borrow checker no longer rejects code just because references are still in scope; it rejects code where references are actually used in conflicting ways.
let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{}", first);
// first is last used here
v.push(4); // OK: no outstanding borrows
Without non-lexical lifetimes, this would fail because first would be considered borrowed until the end of the scope. With them, the borrow ends after the println!, and the mutation is allowed.
e. Mutable and immutable in the same expression
The borrowing rules apply even within a single expression. A common pitfall is trying to mutate a collection while iterating over it:
let mut v = vec![1, 2, 3];
// Error: cannot borrow v as mutable because it is also borrowed as immutable
for item in &v {
v.push(*item);
}
The for loop borrows v immutably for the duration of the iteration, and the v.push inside the loop tries to borrow it mutably. The compiler rejects this.
The fix is to iterate over a clone or a snapshot, or to collect the changes and apply them after:
let v = vec![1, 2, 3];
let mut new_items = Vec::new();
for item in &v {
new_items.push(*item);
}
let mut v = v;
v.extend(new_items);
Or, when the change is a transformation, use a functional approach:
let v = vec![1, 2, 3];
let doubled: Vec<i32> = v.iter().map(|n| n * 2).collect();
The functional methods borrow immutably and produce a new collection, avoiding the conflict.
Another common case is indexing:
let mut v = vec![1, 2, 3];
let first = &v[0];
// v.push(4); // Error if first is used after this
println!("{}", first);
With non-lexical lifetimes, the borrow ends after the println!, so the push is allowed if it comes after. But if the push comes before the println!, the borrow is still live and the code is rejected.
f. Dereferencing and reborrowing
A reference is used through the dereference operator *. For most operations, the dereference is implicit.
let x = 5;
let r = &x;
println!("{}", *r); // 5 (explicit dereference)
println!("{}", r); // 5 (implicit through Display)
For a mutable reference, the dereference is needed to modify the value:
let mut x = 5;
let r = &mut x;
*r += 1;
println!("{}", x); // 6
Rust also reborrows automatically when passing a reference to a function. Passing &mut s to a function that takes &mut T reborrows rather than moving the reference, so the original reference remains usable after the call in some cases.
fn modify(s: &mut String) {
s.push_str("!");
}
let mut s = String::from("hello");
let r = &mut s;
modify(r); // reborrows r
modify(r); // OK: r is still valid
The reborrow is temporary, and the original reference is restored after the call. This is what allows chaining mutable method calls:
let mut v = vec![1, 2, 3];
v.push(4);
v.push(5);
v.push(6);
Each push takes a mutable reference to v, and the borrow ends when the call returns. The next push creates a new borrow.
Complete Example Session
// ============================================
// PART 1: IMMUTABLE REFERENCE
// ============================================
fn length(s: &String) -> usize {
s.len()
}
fn main() {
let s = String::from("hello");
let n = length(&s);
println!("{} has {} characters", s, n);
}
// ============================================
// PART 2: MULTIPLE IMMUTABLE REFERENCES
// ============================================
fn main() {
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{} {} {}", r1, r2, r3);
}
// ============================================
// PART 3: MUTABLE REFERENCE
// ============================================
fn add_exclamation(s: &mut String) {
s.push_str("!");
}
fn main() {
let mut s = String::from("hello");
add_exclamation(&mut s);
println!("{}", s); // hello!
}
// ============================================
// PART 4: EXCLUSIVITY OF MUTABLE
// ============================================
fn main() {
let mut s = String::from("hello");
let r = &mut s;
r.push_str("!");
// let r2 = &s; // Error if uncommented
// let r3 = &mut s; // Error if uncommented
}
// ============================================
// PART 5: NON-LEXICAL LIFETIMES
// ============================================
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
// r1 and r2 end here
let r3 = &mut s;
r3.push_str("!");
println!("{}", r3);
}
// ============================================
// PART 6: VECTOR BORROW ENDS
// ============================================
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{}", first);
// first ends here
v.push(4); // OK
println!("{:?}", v);
}
// ============================================
// PART 7: CONFLICT — MUTATE WHILE ITERATING
// ============================================
// This does not compile:
//
// let mut v = vec![1, 2, 3];
// for item in &v {
// v.push(*item);
// }
//
// Error: cannot borrow `v` as mutable because it is also
// borrowed as immutable
// ============================================
// PART 8: FIX WITH COLLECT
// ============================================
fn main() {
let v = vec![1, 2, 3];
let doubled: Vec<i32> = v.iter().map(|n| n * 2).collect();
println!("{:?}", doubled); // [2, 4, 6]
println!("{:?}", v); // [1, 2, 3]
}
// ============================================
// PART 9: DANGLING REFERENCE
// ============================================
// This does not compile:
//
// fn dangling() -> &String {
// let s = String::from("hello");
// &s
// }
//
// Error: missing lifetime specifier / borrowed value
// does not live long enough
// ============================================
// PART 10: REBORROW
// ============================================
fn modify(s: &mut String) {
s.push_str("!");
}
fn main() {
let mut s = String::from("hello");
let r = &mut s;
modify(r); // reborrows
modify(r); // still valid
println!("{}", s); // hello!!
}
These ten parts cover immutable references, multiple immutable references, mutable references, exclusivity of mutable, non-lexical lifetimes, borrow ending in a vector, the conflict of mutating while iterating, the fix with collect, the dangling reference error, and reborrowing.
Quick Reference
Reference Syntax
| Form | Meaning |
|---|---|
&T | Immutable reference |
&mut T | Mutable reference |
*r | Dereference |
&value | Create a reference |
&mut value | Create a mutable reference |
Borrowing Rules
| Rule | Meaning |
|---|---|
| Aliasing XOR mutation | Either one &mut T or many &T |
| References valid | A reference cannot outlive its referent |
Allowed Combinations
| Combination | Allowed |
|---|---|
Many &T | Yes |
One &mut T | Yes |
&T and &mut T together | No |
Two &mut T | No |
Function Parameters
| Parameter | Effect |
|---|---|
s: String | Takes ownership |
s: &String | Borrows immutably |
s: &mut String | Borrows mutably |
s: &str | Borrows a string slice |
s: &[T] | Borrows a slice |
Non-Lexical Lifetimes
| Situation | Result |
|---|---|
| Borrow not used after a point | Ends there |
| Borrow used later | Still active |
| Overlapping borrows | Error |
Best Practices
✅ Do This:
// Prefer &str over &String
fn length(s: &str) -> usize { s.len() }
// Use &mut only when modification is needed
fn append(s: &mut String) { s.push_str("!"); }
// Let borrows end naturally
let r = &v[0];
println!("{}", r);
v.push(4); // OK after r is last used
// Use functional methods to avoid conflicts
let doubled: Vec<i32> = v.iter().map(|n| n * 2).collect();
❌ Don’t Do This:
// Take ownership when borrowing suffices
fn length(s: String) -> usize { s.len() } // ❌
// Mutate while iterating
for item in &v { v.push(*item); } // ❌
// Return a reference to a local
fn dangling() -> &String { &String::from("x") } // ❌
// Hold a mutable and immutable reference
let r1 = &s;
let r2 = &mut s; // ❌ if r1 is used later
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Cannot borrow as mutable | Immutable borrow still live | Let the immutable borrow end first |
| Cannot borrow as immutable | Mutable borrow still live | Let the mutable borrow end first |
| Dangling reference | Returning a reference to a local | Return an owned value |
| Mutation during iteration | for holds an immutable borrow | Collect first or use iter_mut |
| Two mutable borrows | Overlapping scopes | Separate the scopes |
&String instead of &str | Less general | Use &str |
Real-World Examples
1. Read Without Owning
fn length(s: &str) -> usize { s.len() }
2. Modify Without Owning
fn append(s: &mut String) { s.push_str("!"); }
3. Multiple Readers
let r1 = &data;
let r2 = &data;
4. One Writer
let w = &mut data;
w.push(1);
5. Borrow Ends
let first = &v[0];
println!("{}", first);
v.push(4);
6. Collect to Avoid Conflict
let doubled: Vec<_> = v.iter().map(|n| n * 2).collect();
7. iter_mut for Mutation
for item in v.iter_mut() {
*item *= 2;
}
8. Return Owned
fn create() -> String { String::from("new") }
9. Slice Parameter
fn sum(values: &[i32]) -> i32 {
values.iter().sum()
}
10. Reborrow
modify(&mut s);
modify(&mut s);
Visual
Borrowing Rules
┌──────────────────────────────────────────────────────────────┐
│ ALLOWED: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Many immutable references │ │
│ │ let r1 = &v; │ │
│ │ let r2 = &v; │ │
│ │ let r3 = &v; │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ One mutable reference │ │
│ │ let w = &mut v; │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ NOT ALLOWED: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Immutable and mutable together │ │
│ │ let r = &v; │ │
│ │ let w = &mut v; // Error │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Two mutable references │ │
│ │ let w1 = &mut v; │ │
│ │ let w2 = &mut v; // Error │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Non-Lexical Lifetimes
┌──────────────────────────────────────────────────────────────┐
│ let mut s = String::from("hello"); │
│ │
│ let r1 = &s; ← borrow starts │
│ let r2 = &s; │
│ println!("{} {}", r1, r2); ← borrow ends here │
│ │
│ let r3 = &mut s; ← mutable borrow starts (OK) │
│ r3.push_str("!"); │
│ println!("{}", r3); ← mutable borrow ends │
│ │
│ The borrow checker tracks the last use of each reference, │
│ not the lexical scope. │
└──────────────────────────────────────────────────────────────┘
Dangling Reference
┌──────────────────────────────────────────────────────────────┐
│ fn dangling() -> &String { │
│ let s = String::from("hello"); │
│ &s │
│ } │
│ │
│ s is created in the function │
│ s is dropped at the end of the function │
│ the returned reference would point to freed memory │
│ │
│ Error: `s` does not live long enough │
│ │
│ Fix: return String (owned) instead of &String │
└──────────────────────────────────────────────────────────────┘
Mutation During Iteration
┌──────────────────────────────────────────────────────────────┐
│ for item in &v { │
│ v.push(*item); // Error │
│ } │
│ │
│ The for loop holds an immutable borrow of v │
│ The push requires a mutable borrow of v │
│ They overlap │
│ │
│ Fix options: │
│ 1. Collect new items, then extend │
│ 2. Use iter().map().collect() to build a new vector │
│ 3. Use indices (less idiomatic) │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Immutable reference | &T |
| Mutable reference | &mut T |
| Rule 1 | One &mut T XOR many &T |
| Rule 2 | Reference cannot outlive referent |
| Non-lexical lifetimes | Borrow ends at last use |
| Dereference | *r |
| Reborrow | Passing &mut reborrows automatically |
| Dangling reference | Rejected at compile time |
| Mutation during iteration | Rejected by borrow checker |
| Preferred string parameter | &str |
| Preferred slice parameter | &[T] |
Key takeaways:
- A reference borrows a value without taking ownership.
&Tallows reading;&mut Tallows reading and modifying. The owner retains responsibility for the value. - At any time, a value has either one mutable reference or many immutable references. This is the aliasing XOR mutation rule. It prevents data races and stale reads.
- References cannot outlive the value they point to. The borrow checker rejects code that returns a reference to a local or that uses a reference after its referent is dropped.
- Non-lexical lifetimes end a borrow at its last use. A reference is no longer considered active after the last point where it is used, which makes many patterns work that were previously rejected.
- Mutation during iteration is rejected. A
forloop over a collection holds an immutable borrow, so mutating the collection inside the loop conflicts. Collect first or useiter_mutfor in-place modification. - Prefer
&strover&Stringand&[T]over&Vec<T>. The slice and string-slice types accept more inputs through deref coercion and are the idiomatic parameter types. - Reborrowing is automatic. Passing a mutable reference to a function reborrows it, so the original reference remains usable after the call.
Remember: References and borrowing are how Rust lets you use a value without taking ownership. The rules are simple: either one writer or many readers, and no reference outlives its referent. These rules are enforced at compile time by the borrow checker, and they are the reason Rust code that compiles is free from data races and dangling pointers. The rules feel restrictive at first, but they are what makes safe concurrency possible and what prevents the subtle bugs that plague C and C++. Non-lexical lifetimes make the rules more ergonomic by ending borrows at their last use rather than at the end of the scope. When the borrow checker rejects code, the fix is usually to shorten the lifetime of a borrow, to use a reference instead of an owned value, or to restructure so that the conflicting borrows do not overlap. Understanding references is understanding how Rust separates ownership from use.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!