References & Borrowing
Moving ownership every time you want to use a value would be exhausting. You would have to pass values into functions and get them back out constantly. Borrowing solves this: you lend a value to someone without giving up ownership. References are how Rust implements borrowing, and the rules governing them are what prevent data races at compile time.
Immutable References: &T
An immutable reference lets you read a value without owning it. You can have as many immutable references as you want simultaneously.
fn calculate_length(s: &String) -> usize {
s.len()
// s goes out of scope, but since it's a reference,
// the original String is not dropped
}
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // borrow s
println!("'{}' has length {}", s, len); // s is still valid
}
'hello' has length 5
The & creates a reference. The function receives a reference to the String without taking ownership. After the function returns, s is still valid in main.
Multiple immutable borrows are fine:
fn main() {
let data = vec![1, 2, 3, 4, 5];
let r1 = &data;
let r2 = &data;
let r3 = &data;
println!("r1: {:?}, r2: {:?}, r3: {:?}", r1, r2, r3);
}
r1: [1, 2, 3, 4, 5], r2: [1, 2, 3, 4, 5], r3: [1, 2, 3, 4, 5]
This is safe because nobody is modifying the data. Multiple readers, no writers — no problem.
Mutable References: &mut T
A mutable reference lets you modify a borrowed value. The critical rule: you can have exactly one mutable reference to a value at a time, and no immutable references can coexist with it.
fn append_world(s: &mut String) {
s.push_str(", world");
}
fn main() {
let mut s = String::from("hello");
append_world(&mut s);
println!("{}", s);
}
hello, world
Note that both the variable (let mut s) and the reference (&mut s) must be marked mutable. Rust makes mutation visible at every point in the chain.
The Exclusivity Rule
You cannot have a mutable reference while immutable references exist. This is not arbitrary — it prevents data races.
fn main() {
let mut s = String::from("hello");
let r1 = &s; // immutable borrow
let r2 = &s; // another immutable borrow — fine
// let r3 = &mut s; // error: cannot borrow as mutable
// because it's also borrowed as immutable
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point
let r3 = &mut s; // now this is fine — no immutable borrows are active
r3.push_str("!");
println!("{}", r3);
}
hello and hello
hello!
The borrow checker uses Non-Lexical Lifetimes (NLL): a reference's lifetime ends at its last use, not at the end of the scope. This is why r3 works after r1 and r2 are done being used.
Why This Prevents Data Races
A data race requires three conditions:
- Two or more pointers access the same data simultaneously
- At least one pointer is writing
- There is no synchronization
Rust's borrowing rules make condition 1+2 impossible at compile time. Either you have multiple readers (all immutable references) or one writer (one mutable reference). Never both.
use std::thread;
fn main() {
let mut data = vec![1, 2, 3];
// This would not compile — Rust prevents the data race:
// let r = &data;
// thread::spawn(move || {
// data.push(4); // would need &mut data
// });
// println!("{:?}", r); // r still borrowed immutably
// The safe version uses proper synchronization:
let handle = thread::spawn(move || {
data.push(4); // data moved into the thread
data
});
let data = handle.join().unwrap();
println!("{:?}", data);
}
[1, 2, 3, 4]
References in Structs & Functions
Passing references is the bread and butter of Rust APIs. Prefer borrowing over owning when a function only needs to read data:
struct Config {
database_url: String,
max_retries: u32,
}
// Takes a reference — does not consume the Config
fn connect(config: &Config) -> Result<(), String> {
println!("Connecting to {} (max retries: {})",
config.database_url, config.max_retries);
Ok(())
}
// Takes a mutable reference — modifies the Config in place
fn set_retries(config: &mut Config, retries: u32) {
config.max_retries = retries;
}
fn main() {
let mut config = Config {
database_url: String::from("postgres://localhost/mydb"),
max_retries: 3,
};
connect(&config).unwrap();
set_retries(&mut config, 5);
connect(&config).unwrap();
}
Connecting to postgres://localhost/mydb (max retries: 3)
Connecting to postgres://localhost/mydb (max retries: 5)
Slices: References to Parts of Collections
Slices are references to a contiguous sequence of elements, not the whole collection:
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[..i];
}
}
s
}
fn sum_first_three(numbers: &[i32]) -> i32 {
numbers.iter().take(3).sum()
}
fn main() {
let sentence = String::from("hello beautiful world");
let word = first_word(&sentence);
println!("First word: {}", word);
let nums = vec![10, 20, 30, 40, 50];
let total = sum_first_three(&nums);
println!("Sum of first three: {}", total);
}
First word: hello
Sum of first three: 60
&str is a string slice — a reference to a portion of string data. &[i32] is a slice of integers. These are the idiomatic parameter types when you just need to read data.
Reborrowing
You can create an immutable reference from a mutable one. This is called reborrowing and is implicit:
fn print_value(s: &String) {
println!("{}", s);
}
fn main() {
let mut s = String::from("hello");
let r = &mut s;
print_value(r); // implicitly reborrows &*r (immutable from mutable)
r.push_str("!"); // r is still usable
println!("{}", r);
}
hello
hello!
The compiler automatically creates a temporary immutable reference from the mutable one. This is safe because the mutable reference is "paused" during the function call.
Interior Mutability
Sometimes you need to mutate data behind an immutable reference. Rust provides types for this that move the borrow check to runtime:
use std::cell::RefCell;
struct Logger {
messages: RefCell<Vec<String>>,
}
impl Logger {
fn new() -> Self {
Logger {
messages: RefCell::new(Vec::new()),
}
}
fn log(&self, msg: &str) {
// &self is immutable, but RefCell allows mutation
self.messages.borrow_mut().push(msg.to_string());
}
fn dump(&self) {
for msg in self.messages.borrow().iter() {
println!("[LOG] {}", msg);
}
}
}
fn main() {
let logger = Logger::new();
logger.log("Application started");
logger.log("Processing request");
logger.dump();
}
[LOG] Application started
[LOG] Processing request
RefCell<T> enforces borrowing rules at runtime instead of compile time. It will panic if you violate them. Use it sparingly — it exists for cases where the compiler cannot prove safety but you can.
Common Pitfalls
- Holding references longer than needed. If a reference keeps a large data structure alive, move the reference use earlier or restructure the code.
- Trying to return a reference to local data. A function cannot return
&Stringif theStringwas created inside the function — the data will be freed when the function returns. Return the ownedStringinstead. - Confusing
&Stringand&str. Prefer&strin function parameters. It accepts both&Stringand string literals.&Stringis almost never what you want in a function signature. - Using
RefCellwhen restructuring would be cleaner. Interior mutability is a tool of last resort. If you can redesign your data flow to avoid it, do so. - Forgetting that
&mutrequires the binding to bemuttoo. You needlet mut x = ...before you can create&mut x. The variable itself must be declared as mutable.
Key Takeaways
- Immutable references (
&T) allow multiple simultaneous readers. - Mutable references (
&mut T) allow exactly one writer, with no readers. - These rules prevent data races at compile time, not runtime.
- Prefer
&strover&Stringand&[T]over&Vec<T>in function parameters. - The borrow checker uses Non-Lexical Lifetimes: references end at their last use, not the end of scope.
- Interior mutability (
RefCell,Mutex) moves borrow checking to runtime when compile-time checking is insufficient.