Skip to main content

Borrowing & References

TL;DR Instead of transferring ownership, you can borrow a value via a reference. Rust enforces: either many shared borrows (&T) or exactly one exclusive borrow (&mut T) — never both at once.

Reference Rules

  1. At any point you can have either any number of &T or exactly one &mut T
  2. References must always be valid (no dangling pointers)
let mut s = String::from("hello");

let r1 = &s; // ok — shared borrow
let r2 = &s; // ok — multiple shared borrows are fine
println!("{r1} {r2}");
// r1 and r2 last used above — their borrow ends here (NLL)

let r3 = &mut s; // ok — no active shared borrows at this point
r3.push_str(" world");

Shared Borrows (&T)

  • Read-only access
  • Multiple simultaneous borrows allowed
  • Common in function signatures when you only need to read
fn length(s: &String) -> usize {
s.len() // doesn't need ownership, just reads
}

let s = String::from("hello");
let len = length(&s);
println!("{s} has {len} chars"); // s still valid

Prefer &str over &String in function signatures — &str is a string slice and accepts both String references and string literals.

Exclusive Borrows (&mut T)

  • Read-write access
  • Only one &mut T can exist at a time for a given value
  • The original binding must be declared mut
fn append(s: &mut String) {
s.push_str(" world");
}

let mut s = String::from("hello");
append(&mut s);
println!("{s}"); // "hello world"

Why the restriction? Prevents data races: if two threads could both hold &mut T to the same data, you'd have a race condition. The compiler catches this at compile time.

Non-Lexical Lifetimes (NLL)

Since Rust 2018, the borrow checker uses NLL: borrows end at their last use, not at the end of their scope block.

let mut v = vec![1, 2, 3];
let first = &v[0]; // shared borrow
println!("{first}"); // last use of `first` — borrow ends here

v.push(4); // ok: no active shared borrow

Before NLL this would have been a compile error.

Borrow Splitting

The compiler understands struct fields as separate borrows:

struct Point { x: f64, y: f64 }

let mut p = Point { x: 1.0, y: 2.0 };
let x = &mut p.x;
let y = &mut p.y; // ok — different fields
*x += 1.0;
*y += 1.0;

Same field is not allowed:

let x1 = &mut p.x;
let x2 = &mut p.x; // error: p.x already borrowed mutably

Common Pitfalls

Returning a reference to a local:

// won't compile
fn bad() -> &String {
let s = String::from("hello");
&s // s is dropped at end of function — dangling reference
}

Return the owned String instead, or ensure the data lives long enough.

RefCell<T> — runtime borrow checking:

When you need interior mutability (mutate through a shared reference), RefCell<T> moves the borrow check to runtime. It panics if the rules are violated instead of failing at compile time. Use it sparingly.

Ownership ModelWhy Rust has ownership and what the three rules are Arc<Mutex<T>>Shared mutable state across threads using Arc and Mutex