In the previous tutorial, we learned about ownership. We saw that passing a value to a function moves it, and you cannot use it anymore. That works, but it is limiting.
What if a function only needs to read the data? What if it needs to modify it but give it back? You should not have to move ownership every time.
This is where borrowing comes in. Borrowing lets you use a value without taking ownership of it. The value stays with the original owner.
What Is a Reference?
A reference is like a pointer — it points to data owned by someone else. But unlike raw pointers in C, Rust references are always valid. The compiler guarantees this.
You create a reference with &:
fn main() {
let name = String::from("Alex");
let length = calculate_length(&name); // Borrow name
println!("{} has {} characters", name, length); // name still valid!
}
fn calculate_length(s: &String) -> usize {
s.len()
}
The &name creates a reference to name. The function calculate_length borrows the string — it can read it but does not own it. When the function ends, nothing is dropped because it never had ownership.
Stack Heap
┌──────────────┐
│ name: ptr ───┼────────────► [ A | l | e | x ]
│ len: 4 │ ▲
│ cap: 4 │ │
├──────────────┤ │
│ s: ptr ──────┼─────────┘ (s points to name, not to heap)
└──────────────┘
The reference s points to name, not directly to the heap data. When s goes out of scope, it does not free anything.
Immutable References: &T
By default, references are immutable. You can read data but not change it:
fn print_greeting(message: &String) {
println!("Greeting: {}", message);
// message.push_str("!"); // ERROR: cannot modify through &String
}
fn main() {
let greeting = String::from("Hello");
print_greeting(&greeting);
println!("Still have: {}", greeting);
}
You can have many immutable references at the same time. This is safe because nobody is changing the data:
fn main() {
let data = String::from("shared data");
let r1 = &data;
let r2 = &data;
let r3 = &data;
println!("{}, {}, {}", r1, r2, r3); // All fine
}
Think of it like a library book that many people can read at the same time — as long as nobody is writing in it.
Mutable References: &mut T
To modify borrowed data, you need a mutable reference:
fn add_greeting(message: &mut String) {
message.push_str(", welcome!");
}
fn main() {
let mut name = String::from("Alex");
add_greeting(&mut name);
println!("{}", name); // Alex, welcome!
}
Three things are required for mutable borrowing:
- The variable must be declared
mut - The reference must use
&mut - The function parameter must accept
&mut
The Borrowing Rules
Rust enforces two rules at compile time:
- You can have EITHER one mutable reference OR any number of immutable references — but not both at the same time.
- References must always be valid — no dangling references.
Rule 1: One Mutable OR Many Immutable
This code fails:
fn main() {
let mut data = String::from("hello");
let r1 = &data; // Immutable borrow
let r2 = &mut data; // ERROR: mutable borrow while immutable exists
println!("{}, {}", r1, r2);
}
error[E0502]: cannot borrow `data` as mutable because it is
also borrowed as immutable
And you cannot have two mutable references at the same time:
fn main() {
let mut data = String::from("hello");
let r1 = &mut data;
let r2 = &mut data; // ERROR: second mutable borrow
println!("{}, {}", r1, r2);
}
error[E0499]: cannot borrow `data` as mutable more than once
at a time
Why This Rule Exists
This rule prevents data races at compile time. A data race happens when:
- Two or more pointers access the same data at the same time
- At least one of them is writing
- There is no synchronization
In C or C++, data races cause bugs that are hard to find — they happen randomly and often only in production. Rust makes them impossible by checking at compile time.
Non-Lexical Lifetimes (NLL)
The Rust compiler is smart. A reference’s “lifetime” ends at the point where it is last used, not at the end of the block. So this is fine:
fn main() {
let mut data = String::from("hello");
let r1 = &data;
let r2 = &data;
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point
let r3 = &mut data; // OK! Immutable refs are already done
r3.push_str(" world");
println!("{}", r3);
}
This works because r1 and r2 are not used after the first println!. The compiler sees this and allows the mutable borrow.
Dangling References
A dangling reference points to memory that has been freed. Rust prevents this at compile time:
fn dangle() -> &String { // ERROR: returns a reference to dropped data
let s = String::from("hello");
&s // s is dropped at end of function — reference would be invalid
}
error[E0106]: missing lifetime specifier
The fix is to return the owned value instead:
fn no_dangle() -> String {
let s = String::from("hello");
s // Transfer ownership to caller
}
Borrowing with Slices
References work with slices too. A string slice &str is a reference to part of a String:
fn first_word(text: &str) -> &str {
for (i, ch) in text.chars().enumerate() {
if ch == ' ' {
return &text[..i];
}
}
text
}
fn main() {
let sentence = String::from("hello world");
let word = first_word(&sentence);
println!("First word: {}", word); // hello
}
Notice the parameter is &str not &String. This is more flexible — it accepts both &String and &str. This is a common Rust pattern.
UTF-8 note: This
first_wordfunction works correctly for ASCII text. For strings with multi-byte Unicode characters (like emoji or CJK), indexing by char position differs from byte position. Use.char_indices()instead of.enumerate()for full Unicode safety.
Mutable References in Practice
Here is a practical example — a function that filters a list in place:
fn remove_short_names(names: &mut Vec<String>, min_length: usize) {
names.retain(|name| name.len() >= min_length);
}
fn main() {
let mut team = vec![
String::from("Alex"),
String::from("Jo"),
String::from("Sam"),
String::from("Jordan"),
];
remove_short_names(&mut team, 4);
println!("Team: {:?}", team); // ["Alex", "Jordan"]
}
The function borrows team mutably, modifies it, and returns. team is still owned by main.
References to Primitives
Copy types also work with references. Sometimes you want to avoid copies for large arrays or when you need the function to modify the original:
fn double_in_place(value: &mut i32) {
*value *= 2; // Dereference with * to modify the actual value
}
fn main() {
let mut score = 50;
double_in_place(&mut score);
println!("Score: {}", score); // 100
}
The * is the dereference operator. You need it to access the value behind the reference.
Lifetimes Preview
Sometimes the compiler needs help figuring out how long references live. This is what lifetimes are for:
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() >= s2.len() {
s1
} else {
s2
}
}
fn main() {
let result;
let s1 = String::from("long string");
{
let s2 = String::from("short");
result = longer(&s1, &s2);
println!("Longer: {}", result);
}
}
The 'a annotation tells the compiler: “the returned reference lives as long as the shortest of the two input references.” We will cover lifetimes in detail in a future tutorial.
Common Mistakes and Fixes
Mistake: Modifying While Iterating
// ERROR: cannot borrow as mutable while iterating
let mut items = vec![1, 2, 3];
for item in &items {
if *item == 2 {
items.push(4); // ERROR!
}
}
Fix: Collect what you need first, then modify:
let mut items = vec![1, 2, 3];
let needs_four = items.iter().any(|&x| x == 2);
if needs_four {
items.push(4);
}
Mistake: Returning a Reference to a Local
// ERROR: cannot return reference to local variable
fn create_name() -> &str {
let name = String::from("Alex");
&name // name is dropped here!
}
Fix: Return the owned value:
fn create_name() -> String {
String::from("Alex")
}
Summary
| Concept | Syntax | Rules |
|---|---|---|
| Immutable reference | &T | Many allowed at once |
| Mutable reference | &mut T | Only one at a time |
| Borrow rule | — | One &mut OR many &, never both |
| Dereference | *ref | Access value behind reference |
| Slice | &str, &[T] | Reference to part of data |
| No dangling refs | — | Compiler prevents invalid references |
Source Code
Related Tutorials
- Rust Tutorial #4: Ownership — understand ownership first
- Rust Tutorial #6: Structs and Methods — coming next
What’s Next?
We now understand ownership and borrowing — the core of Rust’s memory model. Next, we learn structs and methods — how to create your own types and attach behavior to them. This is where Rust starts to feel like a real application language.