References and Borrowing
When you pass data to a function or store it in another variable in Rust, ownership rules mean the original owner can lose access to that value the moment it moves. Constantly moving or cloning data just to let another piece of code look at it would make Rust exhausting to write. References solve this: they let code access a value without taking ownership of it, so a function can read — or even modify — data that still belongs to someone else. The compile-time rules around references, called borrowing, are the mechanism that lets Rust guarantee memory safety and rule out data races with zero runtime cost.
Overview: How Borrowing Works
Think of a value in Rust as a book with exactly one owner. Handing the book to someone else (moving it) means you no longer have it. Often, though, you just want a friend to read a page, or to jot a note in the margin, without giving the book away for good. A reference is exactly that: a way to lend access to a value while the original owner keeps it and automatically gets it back when the loan ends.
You create a reference with the & operator. &value creates a shared reference (an immutable borrow) — like letting several people read the same book at once, with nobody allowed to write in it. &mut value creates a mutable reference (an exclusive borrow) — like handing someone the only pen allowed near the book; while they hold it, nobody else, not even the owner, may read or write it.
Trace through a small example: let s1 = String::from("hello"); allocates a String, and s1 owns the heap buffer holding its bytes. Calling a function as calculate_length(&s1) does not move s1 — it creates a reference, which under the hood is just a pointer to the same heap data, and passes that pointer in. Inside the function the parameter’s type is &String, meaning “a reference to a String I don’t own.” When the function returns, the reference simply goes out of scope; nothing gets dropped, because a reference never owns the data it points to. Back in the caller, s1 is still completely valid — ownership never left it.
The compiler enforces two rules about references, together called the borrowing rules:
- At any given moment, a value may have either any number of shared references (
&T) or exactly one mutable reference (&mut T) — never both kinds active at the same time. - A reference must never outlive the data it points to — there is no such thing as a valid dangling reference in safe Rust.
These rules look restrictive at first glance, but they are exactly what let Rust catch data races — two parts of a program racing to read and write the same memory at the same time — at compile time, with no garbage collector and no runtime locks. In a language like C, nothing stops you from mutating memory through one pointer while another pointer is busy reading it; that is the root cause of a huge class of memory-safety bugs. Rust’s borrow checker, a part of the compiler, walks through every borrow in your program and statically proves these rules hold before it will produce a binary. If your code violates them, compilation fails with an error explaining exactly which borrows conflict — you find out at cargo build time, not in production.
Syntax
The table below summarizes reference syntax:
| Syntax | Meaning |
|---|---|
&value |
Creates a shared (immutable) reference to value. |
&mut value |
Creates a mutable (exclusive) reference to value. The binding for value must itself be declared with mut. |
fn f(x: &T) |
A function parameter that borrows a T immutably; the caller retains ownership. |
fn f(x: &mut T) |
A function parameter that borrows a T mutably; the caller retains ownership, but the function may modify the value. |
*reference |
The dereference operator — follows a reference to read or write the value it points to. |
In practice, the general shape of borrowing looks like this:
&value // shared (immutable) reference
&mut value // mutable (exclusive) reference
fn takes_shared(x: &i32) {}
fn takes_mutable(x: &mut i32) {}
Examples
Example 1: Borrowing to read a value
The most common reason to use a reference is so a function can inspect a value without taking it over.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
Output:
The length of 'hello' is 5.
calculate_length takes a &String instead of a String, so passing &s1 only lends the function a look at the data. Since ownership was never transferred, s1 is still valid and printable on the very next line — something that would be a compile error if calculate_length had taken s: String by value, because s1 would have moved into the function and been dropped when it returned.
Example 2: Mutable borrowing to modify a value
A mutable reference lets a function change data it doesn’t own.
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s);
}
fn change(s: &mut String) {
s.push_str(", world");
}
Output:
hello, world
s must be declared with mut before it can be borrowed mutably — Rust refuses to let you create a &mut to a binding that isn’t itself mutable. Inside change, the parameter has type &mut String; calling .push_str on it mutates the original String‘s buffer directly, through the reference. No value is copied, cloned, or returned — the caller’s s is changed in place.
Example 3: Multiple immutable borrows and slices
Shared references compose well — you can have as many as you like, and functions that only need to read data should generally take a slice (&[T]) rather than a whole owned collection.
fn main() {
let numbers = vec![10, 20, 30, 40];
let total = sum(&numbers);
let max = find_max(&numbers);
println!("Total: {}, Max: {}", total, max);
println!("Original vector still usable: {:?}", numbers);
}
fn sum(nums: &[i32]) -> i32 {
nums.iter().sum()
}
fn find_max(nums: &[i32]) -> i32 {
*nums.iter().max().unwrap()
}
Output:
Total: 100, Max: 40
Original vector still usable: [10, 20, 30, 40]
&numbers converts the &Vec<i32> into a &[i32] slice reference through deref coercion, so both sum and find_max can borrow the same vector’s contents — first one, then the other — without ever taking ownership. Because each borrow’s lifetime ends when its function call returns, there’s no conflict between the two calls, and numbers remains fully usable afterward. Taking &[i32] instead of &Vec<i32> is also more flexible: it works for vectors, arrays, and slices of arrays alike.
How It Works Step by Step
Walking through the mutable-borrow example (change) the way the compiler does:
sis created inmainand owns the heap-allocated bytes of theString.&mut screates a mutable reference — the compiler checks that no other reference tosis alive at this point, and thatswas declaredmut.- That reference is passed into
change, where the parameter’s type&mut Stringonly grants the ability to read and write through the pointer — it does not grant ownership, sochangecannot drop or move the caller’s data. s.push_str(", world")insidechangedereferences the mutable reference automatically (method call syntax handles this) and appends to the original buffer.- When
changereturns, the mutable borrow’s lifetime ends. Back inmain, no borrows are outstanding, soscan be freely read again in theprintln!.
The slice example follows the same pattern but with shared references: &numbers is borrowed, passed to sum, and that borrow ends the instant sum returns; a fresh &numbers borrow is then created for find_max. Because the two borrows never overlap in time, the “one mutable reference or many shared references” rule is trivially satisfied — and even if they did overlap, two shared borrows together would still be fine, since only mixing a mutable borrow with any other borrow is disallowed.
Common Mistakes
Mistake 1: Mixing a mutable and an immutable borrow
This is the classic borrow-checker error — creating a mutable reference while a shared reference to the same value is still in use.
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
}
This fails to compile with an error like cannot borrow `s` as mutable because it is also borrowed as immutable. r1 is still alive (it gets used in the println! below), so the compiler cannot also hand out a mutable reference r2 — if it did, printing r1 could observe a value that r2 is simultaneously changing. The fix is to make sure the immutable borrow’s last use happens before the mutable one is created; thanks to non-lexical lifetimes (NLL), a reference’s borrow ends at its last use, not at the end of its scope:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1);
let r2 = &mut s;
r2.push_str(", world");
println!("{}", r2);
}
Output:
hello
hello, world
Mistake 2: Returning a dangling reference
Trying to return a reference to data that only exists inside the function is a classic mistake for anyone coming from a language with manual memory management.
fn dangle() -> &String {
let s = String::from("hello");
&s
}
This fails to compile: s is a local variable, dropped the instant dangle returns, so a reference to it would point at freed memory. Rust rejects this at compile time — it won’t even let you write the function signature without a lifetime, and no lifetime can make freed memory valid. The fix is to return owned data instead of a reference:
fn main() {
let s = no_dangle();
println!("{}", s);
}
fn no_dangle() -> String {
let s = String::from("hello");
s
}
Output:
hello
no_dangle returns s by value, which moves ownership of the String out to the caller instead of leaving behind a reference to something that no longer exists.
Mistake 3: Mutating through a shared reference
A shared reference is read-only, even if the underlying method looks like it should work.
fn main() {
let s = String::from("hello");
let r = &s;
r.push_str(", world");
}
This fails to compile with cannot borrow `*r` as mutable, as it is behind a `&` reference, because push_str requires &mut self and r is only a &String. The fix is to make the binding mutable and borrow it mutably from the start:
fn main() {
let mut s = String::from("hello");
let r = &mut s;
r.push_str(", world");
println!("{}", r);
}
Output:
hello, world
Best Practices
- Default to shared references (
&T) for function parameters; only ask for&mut Twhen the function genuinely needs to modify the value. - Prefer
&strover&Stringand&[T]over&Vec<T>in function signatures — both accept the owned type via deref coercion, plus literals, arrays, and slices, so your functions stay usable in more places. - Keep borrows short-lived: use a reference, let it go out of scope (or simply stop using it), and only then create a new, possibly conflicting, borrow. Non-lexical lifetimes reward code that stops using a reference as soon as it’s done with it.
- Never try to return a reference to a value created inside the function that returns it — return an owned value (or accept a reference as input and return a reference derived from that same input).
- Reach for
.clone()when you’re first learning and the borrow checker is blocking you and you don’t yet see a clean borrowing solution — it’s correct, if not always optimal, and you can revisit it once the ownership design settles. - Remember that a method call like
r.push_str(...)orr.len()auto-dereferences for you; you rarely need to write(*r).method()by hand.
Practice Exercises
- Write a function
averagethat takes a&[i32]and returns its average as anf64, without taking ownership of the slice. Call it on the same vector twice inmainand print both results plus the original vector. - Write a function
shoutthat takes a&mut Stringand appends"!"to it. Call it on a mutableStringinmain, then print the string before and after callingshout. - Given a broken snippet that creates
let r1 = &v;and thenlet r2 = &mut v;and uses both afterward, rewrite it so it compiles by reordering the borrows’ last uses. The expected output after your fix should print both the read value and the mutated value with no compiler errors.
Summary
- A reference (
&Tor&mut T) lets code access a value without taking ownership of it. &valuecreates a shared, read-only borrow;&mut valuecreates an exclusive, read-write borrow — and the original binding must bemutto allow the latter.- At any moment, a value can have any number of shared references or exactly one mutable reference, never both — this is checked entirely at compile time.
- References can never outlive the data they point to; the compiler rejects any attempt to create a dangling reference.
- Prefer borrowing over cloning, and prefer the least powerful reference (
&Tover&mut T,&str/&[T]over owned types) in function signatures. - The borrow checker is what lets Rust guarantee no data races and no use-after-free, without a garbage collector.
