| |

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:

  1. At any given time, you can have either one mutable reference or any number of immutable references.
  2. 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

FormMeaning
&TImmutable reference
&mut TMutable reference
*rDereference
&valueCreate a reference
&mut valueCreate a mutable reference

Borrowing Rules

RuleMeaning
Aliasing XOR mutationEither one &mut T or many &T
References validA reference cannot outlive its referent

Allowed Combinations

CombinationAllowed
Many &TYes
One &mut TYes
&T and &mut T togetherNo
Two &mut TNo

Function Parameters

ParameterEffect
s: StringTakes ownership
s: &StringBorrows immutably
s: &mut StringBorrows mutably
s: &strBorrows a string slice
s: &[T]Borrows a slice

Non-Lexical Lifetimes

SituationResult
Borrow not used after a pointEnds there
Borrow used laterStill active
Overlapping borrowsError

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

PitfallWhy It HappensFix
Cannot borrow as mutableImmutable borrow still liveLet the immutable borrow end first
Cannot borrow as immutableMutable borrow still liveLet the mutable borrow end first
Dangling referenceReturning a reference to a localReturn an owned value
Mutation during iterationfor holds an immutable borrowCollect first or use iter_mut
Two mutable borrowsOverlapping scopesSeparate the scopes
&String instead of &strLess generalUse &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

ItemValue
Immutable reference&T
Mutable reference&mut T
Rule 1One &mut T XOR many &T
Rule 2Reference cannot outlive referent
Non-lexical lifetimesBorrow ends at last use
Dereference*r
ReborrowPassing &mut reborrows automatically
Dangling referenceRejected at compile time
Mutation during iterationRejected by borrow checker
Preferred string parameter&str
Preferred slice parameter&[T]

Key takeaways:

  • A reference borrows a value without taking ownership. &T allows reading; &mut T allows 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 for loop over a collection holds an immutable borrow, so mutating the collection inside the loop conflicts. Collect first or use iter_mut for in-place modification.
  • Prefer &str over &String and &[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!