
Rust References and Borrowing: Using `&` and `&mut` Effectively
What references are in Rust
A reference is a non-owning pointer to a value. Instead of moving the value into a function or copying it, you can pass a reference and let the function read or modify the original value under controlled rules.
Rust has two main kinds of references:
&T: immutable reference, used for read-only access&mut T: mutable reference, used for exclusive write access
The key idea is simple:
- Many immutable references can exist at the same time.
- Only one mutable reference can exist at a time.
- A mutable reference cannot coexist with any immutable references to the same value.
These rules are enforced at compile time, which helps prevent data races and invalid memory access.
Basic example
fn main() {
let name = String::from("Rust");
print_length(&name);
println!("Original value is still available: {name}");
}
fn print_length(text: &String) {
println!("Length: {}", text.len());
}The function print_length borrows the string. It can inspect the value, but it does not take ownership, so name remains usable afterward.
Immutable borrowing with &
Immutable references are the default choice when a function only needs to read data. They are lightweight and allow multiple readers at once.
Why immutable borrowing matters
Suppose you want to compute something from a large string, vector, or struct. Taking ownership would force the caller to give up the value, even if the function only needs to inspect it. Borrowing avoids that cost and keeps the caller’s data available.
fn first_word(text: &str) -> &str {
for (i, ch) in text.char_indices() {
if ch == ' ' {
return &text[..i];
}
}
text
}
fn main() {
let sentence = String::from("borrowed data is efficient");
let word = first_word(&sentence);
println!("{word}");
}This example uses &str, not &String. That distinction is important: borrowing the string slice is more flexible because it accepts both String and string literals through deref coercion.
Prefer borrowed parameters when possible
A good rule is to accept the most general borrowed type that fits your needs:
- Use
&strfor text input - Use
&[T]for read-only access to a sequence - Use
&Tfor shared access to a custom type
This makes your functions easier to reuse and reduces unnecessary allocations or cloning.
Mutable borrowing with &mut
Mutable references allow a function to change data in place. This is useful when you want to update a collection, normalize a value, or accumulate results without returning a new object every time.
Example: modifying a value in place
fn append_suffix(text: &mut String) {
text.push_str("!");
}
fn main() {
let mut message = String::from("Hello");
append_suffix(&mut message);
println!("{message}");
}Notice two requirements:
- The variable must be declared
mut. - The reference passed to the function must be
&mut message.
Both are necessary. The first allows the variable itself to be mutated. The second grants the function exclusive access.
Exclusive access is the point
Rust prevents mutable borrowing conflicts because simultaneous mutation is dangerous. If one part of the program can change a value while another part reads it, the result may be inconsistent or undefined in languages without strong safety checks.
Rust’s rule is stricter but safer: while a mutable borrow exists, no other borrow of the same value is allowed.
Borrowing rules in practice
The borrow checker enforces a few core rules that shape everyday Rust code.
| Rule | Meaning | Practical impact |
|---|---|---|
| One mutable borrow at a time | Exclusive write access | Prevents conflicting updates |
| Any number of immutable borrows | Shared read access | Enables multiple readers |
| No mixing mutable and immutable borrows | Reads and writes cannot overlap | Forces clear access boundaries |
Example of valid borrowing
fn main() {
let data = String::from("abc");
let r1 = &data;
let r2 = &data;
println!("{r1} {r2}");
}Multiple immutable references are fine because neither can modify the data.
Example of invalid borrowing
fn main() {
let mut data = String::from("abc");
let r1 = &data;
let r2 = &mut data;
println!("{r1}");
println!("{r2}");
}This does not compile because r1 and r2 overlap. Rust rejects the code before it can become a runtime bug.
Borrow scopes and non-lexical lifetimes
In modern Rust, a borrow ends when it is no longer used, not necessarily at the end of the block. This is called non-lexical lifetimes, and it makes code more ergonomic.
fn main() {
let mut value = String::from("hello");
let first = &value;
println!("{first}");
let second = &mut value;
second.push_str(" world");
println!("{second}");
}Here, the immutable borrow ends after println!("{first}"), so the mutable borrow can begin afterward.
Practical takeaway
If you get a borrow error, check whether a reference is still being used later than you expect. Often the fix is to shorten the borrow’s scope by:
- moving a
println!earlier - introducing a smaller block
- avoiding storing a reference longer than needed
fn main() {
let mut value = String::from("hello");
{
let first = &value;
println!("{first}");
}
let second = &mut value;
second.push_str(" world");
}The inner block makes the lifetime boundary explicit.
Borrowing slices and collections
Borrowing is not limited to strings. It is especially useful with vectors and arrays, where you often want to inspect or update part of a collection without taking ownership.
Read-only access to a slice
fn sum(values: &[i32]) -> i32 {
values.iter().sum()
}
fn main() {
let numbers = vec![1, 2, 3, 4];
let total = sum(&numbers);
println!("{total}");
}The function accepts a slice, which is a borrowed view into a sequence. This is more flexible than requiring a Vec<i32>.
Mutable access to a slice
fn double_all(values: &mut [i32]) {
for value in values {
*value *= 2;
}
}
fn main() {
let mut numbers = vec![1, 2, 3, 4];
double_all(&mut numbers);
println!("{numbers:?}");
}This pattern is common in data processing code because it updates values in place without allocating a new vector.
Common compiler errors and how to read them
Borrowing errors can look intimidating at first, but they usually point to a clear rule violation.
“cannot borrow as mutable because it is also borrowed as immutable”
This means you tried to create a mutable reference while an immutable one was still active.
Typical fixes:
- stop using the immutable reference earlier
- clone the data if you truly need independent ownership
- restructure the code so reads happen before writes
“cannot move out of borrowed content”
This happens when you try to take ownership of a field or value through a reference. Borrowed data is not yours to move.
Instead, consider:
- borrowing the field as
&T - cloning the field if ownership is needed
- using methods that operate on references
“borrowed value does not live long enough”
This often appears when a reference escapes the scope of the value it points to. The fix is usually to return owned data instead of a reference, or to ensure the original value lives long enough.
Designing APIs around borrowing
Good Rust APIs often use borrowing to make usage efficient and predictable. The goal is to accept references when ownership is unnecessary and reserve ownership for cases where the function must store or transform data independently.
Guidelines for function signatures
| Situation | Recommended parameter type | Why |
|---|---|---|
| Read text | &str | Works with String and string literals |
| Read a list | &[T] | Works with vectors and arrays |
| Modify text in place | &mut String | Avoids allocation |
| Modify a list in place | &mut [T] | Efficient bulk updates |
| Store data beyond the call | Owned T | Caller transfers responsibility |
Example: a practical parser helper
fn is_valid_username(name: &str) -> bool {
let trimmed = name.trim();
!trimmed.is_empty() && trimmed.len() >= 3
}
fn main() {
let input = String::from(" alice ");
println!("{}", is_valid_username(&input));
}The function only needs to inspect the input, so borrowing is the best choice.
Example: in-place normalization
fn normalize_username(name: &mut String) {
let trimmed = name.trim().to_lowercase();
*name = trimmed;
}
fn main() {
let mut username = String::from(" Alice ");
normalize_username(&mut username);
println!("{username}");
}Here mutation is appropriate because the function intentionally updates the caller’s value.
Best practices for using references
1. Borrow by default for read-only operations
If a function does not need ownership, take a reference. This keeps APIs efficient and reduces unnecessary cloning.
2. Use the most specific borrowed type that fits
Prefer &str over &String, and &[T] over &Vec<T>. This makes your functions more general and easier to compose.
3. Keep mutable borrows short
Short mutable borrow scopes make code easier to reason about and reduce borrow checker friction.
4. Avoid cloning just to satisfy the compiler
Cloning can be correct, but it should be intentional. If you are cloning only because the borrow checker complained, first consider whether the code structure can be improved.
5. Separate reading and writing phases
A common pattern is to collect all needed reads first, then perform mutations afterward. This aligns naturally with Rust’s borrowing rules.
When borrowing is not the right choice
Borrowing is powerful, but it is not always the best design.
Use ownership when:
- the function must store the value for later use
- the data outlives the caller’s scope
- the function transforms data into a new owned result
- the API needs to move values between threads or tasks
Borrowing is ideal when the function only needs temporary access. Ownership is better when the function becomes responsible for the data.
Summary
References are the everyday mechanism that lets Rust code access data safely without unnecessary copying. Immutable references support shared reads, mutable references support exclusive updates, and the borrow checker ensures those access patterns do not overlap in unsafe ways.
If you remember only a few rules, make them these:
- Use
&Tfor read-only access. - Use
&mut Tfor in-place mutation. - Prefer borrowed inputs like
&strand&[T]in APIs. - Keep borrow scopes short and explicit.
- Let ownership handle cases where data must be stored or transferred.
Once these patterns become familiar, borrowing stops feeling restrictive and starts feeling like a design tool.
