Skip to main content

Borrowing

Borrowing lets you use a value without taking ownership of it. You do this with references.

  • Immutable borrow → &T
  • Mutable borrow → &mut T

Rust enforces strict rules so memory stays safe at compile time, with zero runtime cost.

Why Borrowing Exists

Without borrowing, you’d constantly move ownership into functions and back out:

fn print(s: String) -> String {
println!("{}", s);
s
}

That’s clunky.

Borrowing solves this by allowing temporary access without transferring ownership.

Immutable Borrowing (&T)

  • Read-only access
  • Any number of immutable borrows allowed
  • Ownership stays with the original variable
fn print_length(s: &String) {
println!("Length: {}", s.len());
}

fn main() {
let s = String::from("hello");

print_length(&s);
print_length(&s);

println!("{}", s); // still valid
}
  1. s owns the String
  2. &s creates an immutable reference
  3. The function borrows the value
  4. No ownership is transferred
  5. Multiple immutable borrows are allowed

Think of immutable borrows as many people reading the same book—no one can change it.

Mutable Borrowing (&mut T)

  • Read and write access
  • Exactly one mutable borrow at a time
  • The original value must be declared mut
fn add_world(s: &mut String) {
s.push_str(" world");
}

fn main() {
let mut s = String::from("hello");

add_world(&mut s);
println!("{}", s); // "hello world"
}
  1. s owns the String
  2. &mut s creates a mutable reference
  3. The function can modify the data
  4. Ownership remains in main

Think of mutable borrowing as one person editing the document—no one else can even read it at the same time.

The Golden Borrowing Rules

Rust enforces these rules at compile time:

Rule 1: Multiple immutable borrows OR one mutable borrow

✅ Allowed:

let r1 = &s;
let r2 = &s;

✅ Allowed:

let r = &mut s;

❌ Not allowed:

let r1 = &s;
let r2 = &mut s; // ERROR

❌ Not allowed:

let r1 = &mut s;
let r2 = &mut s; // ERROR

Why this rule exists

It prevents data races:

  • One thread reads while another writes → inconsistent state
  • Two writers at once → corrupted data

Rust stops these problems before your code runs.

Borrow Scopes (Non-Lexical Lifetimes)

Rust is smarter than it looks.

let mut s = String::from("hello");

let r1 = &s;
println!("{}", r1); // last use of r1

let r2 = &mut s; // ✅ allowed
r2.push_str(" world");

Even though r1 is still “in scope” textually, Rust sees it’s no longer used and ends the borrow early.

This is called Non-Lexical Lifetimes (NLL).

Mutable + Immutable Borrowing (Common Error)

let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;

let r3 = &mut s; // ❌ error

Fix it like this:

let mut s = String::from("hello");

{
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
} // r1 and r2 go out of scope

let r3 = &mut s; // ✅ OK

Borrowing in Functions

Immutable borrow

fn calculate_length(s: &String) -> usize {
s.len()
}

Mutable borrow

fn append_exclamation(s: &mut String) {
s.push('!');
}

Using both safely

fn main() {
let mut s = String::from("Rust");

let len = calculate_length(&s);
append_exclamation(&mut s);

println!("{} ({})", s, len);
}

Borrowing Prevents Dangling References

Rust will not let references outlive the data they point to.

fn get_ref() -> &String {
let s = String::from("hello");
&s // ❌ compile-time error
}

Why?

  • s is dropped at the end of the function
  • The reference would point to freed memory

Rust refuses to compile this.

Borrowing with Slices (Very Common)

Slices are references too.

fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();

for (i, &b) in bytes.iter().enumerate() {
if b == b' ' {
return &s[0..i];
}
}

&s[..]
}
  • &str is an immutable borrow of a string slice
  • No allocation
  • No ownership transfer

Mental Model (This Helps a LOT)

  • Ownership: who frees the memory?
  • Immutable borrow (&): read-only, many allowed
  • Mutable borrow (&mut): read-write, only one allowed
  • Compiler = strict librarian

If Rust allows it, it’s safe. Period.

Why Borrowing Is a Big Deal

Borrowing gives Rust:

  • Memory safety
  • Thread safety
  • Zero runtime overhead
  • No data races
  • No dangling pointers

And you get these guarantees without writing extra code.