Borrowing & References in Rust
Rust's ownership system is powerful, but if ownership were the only mechanism, working with data would be cumbersome — you'd have to pass values into functions and return them back just to continue using them. Borrowing solves this by letting you temporarily use a value without taking ownership of it.
The Problem With Moving
When you pass a value to a function in Rust, ownership moves into that function. Once the function returns, you no longer have access to that value — unless the function explicitly returns it back to you.
fn main() {
let s = String::from("hello");
let len = calculate_length(s); // ownership moves here
// s is no longer valid — we can't use it!
// println!("{}", s); // ERROR: value borrowed here after move
println!("Length: {}", len);
}
fn calculate_length(s: String) -> usize {
s.len()
} // s is dropped hereThis works, but it's awkward. What if you need to use s again after calling the function? You'd have to return it alongside the length:
fn main() {
let s = String::from("hello");
// Transfer ownership in, get it back out
let (s, len) = calculate_length(s);
println!("'{}' has length {}", s, len); // now we can use s again
}
fn calculate_length(s: String) -> (String, usize) {
let len = s.len();
(s, len) // return ownership back to caller
}What Is Borrowing?
Borrowing means creating a reference to a value. A reference lets you use a value without taking ownership of it. When the reference goes out of scope, the original value is not dropped — because the reference never owned it.
Think of it like lending a book to a friend. Your friend can read the book, but they don't own it. When they're done, the book comes back to you automatically.
You create a reference using the & operator:
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // pass a reference — not ownership
println!("'{}' has length {}", s, len); // s still valid!
}
fn calculate_length(s: &String) -> usize { // s is a reference to a String
s.len()
} // s goes out of scope, but nothing is dropped — it doesn't own the dataImmutable References: &T
A plain reference &T is an immutable reference. It gives read-only access to the data. You cannot modify the value through an immutable reference — only read from it.
This is the default behaviour. References are immutable unless you explicitly opt in to mutability (covered in the next section).
fn main() {
let s = String::from("hello");
let r = &s; // r is an immutable reference to s
println!("{}", r); // fine — reading is allowed
println!("{}", s); // also fine — s is still the owner
// r.push_str("!"); // ERROR: cannot borrow as mutable
// *r = String::from("world"); // ERROR: cannot assign through &String
}Creating and Using References
There are two sides to every reference:
- Creating a reference: use
&before the value (the caller's side) - Accepting a reference: use
&Tin the function parameter type (the callee's side)
The act of creating a reference is called borrowing.
fn greet(name: &String) { // accepts a reference to String
println!("Hello, {}!", name);
}
fn main() {
let name = String::from("Alice");
greet(&name); // borrow name — pass a reference
greet(&name); // borrow again — still valid!
greet(&name); // and again — immutable refs can be reused freely
println!("name is still: {}", name); // ownership never left main
}Multiple Immutable References
You can have any number of immutable references to a value at the same time. Since none of them can modify the data, they can safely coexist — there's no risk of one reference changing data while another is reading it.
fn main() {
let s = String::from("hello");
let r1 = &s; // first reference
let r2 = &s; // second reference
let r3 = &s; // third reference — all fine!
println!("{}, {}, {}", r1, r2, r3); // all valid simultaneously
}References Don't Own the Data
A critical property of references: they do not own the data they point to. This means:
- When a reference goes out of scope, the underlying data is not dropped
- The original owner remains responsible for cleanup
- The reference is only valid as long as the original owner is alive
fn main() {
let s = String::from("hello"); // s owns the String
{
let r = &s; // r borrows s
println!("{}", r); // use r
} // r goes out of scope — String is NOT dropped
// (r didn't own it)
println!("{}", s); // s still valid — it's still the owner
} // s goes out of scope — String IS dropped hereDereferencing: *r
To access the value that a reference points to, you use the dereference operator *. In many situations Rust performs automatic dereferencing, so you don't need to write it explicitly — but understanding how it works is important.
fn main() {
let x = 5;
let r = &x; // r is a reference to x
println!("{}", r); // Rust auto-derefs — prints 5
println!("{}", *r); // explicit deref — also prints 5
// Comparison works with auto-deref
if *r == 5 {
println!("r points to 5");
}
// Auto-deref in method calls
let s = String::from("hello");
let sr = &s;
println!("{}", sr.len()); // same as (*sr).len() — auto-deref
}Dangling References: How Rust Protects You
In languages like C and C++, it's easy to create a dangling pointer — a reference to memory that has already been freed. This causes undefined behaviour and is a major source of security vulnerabilities and crashes.
Rust prevents dangling references entirely at compile time. The borrow checker ensures that a reference can never outlive the data it refers to.
// This does NOT compile in Rust
fn dangle() -> &String { // trying to return a reference
let s = String::from("hello"); // s is created here
&s // try to return a reference to s
} // s is dropped here — reference would dangle!error[E0106]: missing lifetime specifier
--> src/main.rs:1:16
|
1 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value,
but there is no value for it to be borrowed fromThe compiler refuses to compile this code. The fix is to return the String itself, transferring ownership to the caller instead of trying to return a reference:
fn no_dangle() -> String {
let s = String::from("hello");
s // move ownership out — caller takes responsibility for cleanup
}
fn main() {
let s = no_dangle();
println!("{}", s); // perfectly safe
}Slices as References
Slices are a special kind of reference. They refer to a contiguous sequence of elements in a collection rather than the whole collection. Two common slice types are:
&str— a string slice (a reference into aStringor a string literal)&[T]— a slice of a vector or array
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // slice: bytes 0–4 → "hello"
let world = &s[6..11]; // slice: bytes 6–10 → "world"
println!("{} {}", hello, world); // hello world
// String literals are already slices (&str)
let literal: &str = "I am a string slice";
// Vector slices
let v = vec![1, 2, 3, 4, 5];
let first_three: &[i32] = &v[0..3];
println!("{:?}", first_three); // [1, 2, 3]
}// Flexible: accepts both &str and &String (via deref coercion)
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[0..i]; // return slice up to first space
}
}
&s[..] // no space found — return whole string
}
fn main() {
let owned = String::from("hello world");
let literal = "hello world";
println!("{}", first_word(&owned)); // &String — works via deref coercion
println!("{}", first_word(literal)); // &str — works directly
println!("{}", first_word(&owned[6..])); // slice of String — also works
}References and Lifetimes
Every reference has a lifetime — the scope for which it is valid. Rust tracks lifetimes to ensure that references never outlive the data they point to.
For most everyday code, the compiler infers lifetimes automatically through lifetime elision rules. You only need to annotate them explicitly in more complex situations (covered in the Lifetimes section).
The core rule: a reference's lifetime must be entirely contained within the owner's lifetime.
fn main() {
let r; // r declared — no value yet
{
let x = 5;
r = &x; // r borrows x
println!("{}", r); // OK — x is still alive here
} // x is dropped here
// println!("{}", r); // ERROR — r would outlive x!
}error[E0597]: `x` does not live long enough
--> src/main.rs:7:13
|
6 | r = &x;
| ^^ borrowed value does not live long enough
8 | }
| - `x` dropped here while still borrowed
9 |
10| println!("{}", r);
| - borrow later used hereBefore and After: Why Borrowing Matters
Without Borrowing | With Borrowing |
|---|---|
Must return value to regain access | Original still accessible after function call |
fn f(s: String) — takes ownership | fn f(s: &String) — borrows only |
Caller uses tuple to get value back | Caller passes &s, keeps using s freely |
Every use of a value involves moving | Multiple functions can read the same value |
Code is verbose and awkward | Code is clean and natural |
The Two Rules of References
Rust enforces exactly two rules about references at compile time:
At any given time, you can have either any number of immutable references, or exactly one mutable reference — never both simultaneously.
References must always be valid. A reference can never outlive the data it points to (no dangling references).
Quick Reference: Borrowing Syntax
Syntax | Meaning |
|---|---|
| Create an immutable reference to value |
| Type annotation for an immutable reference to T |
| Dereference r to get the underlying value |
| Create a slice reference (elements 0 to 4) |
| A string slice type (immutable reference to string data) |
| A slice of type T (reference into an array or vec) |
Summary
Borrowing lets you use a value without taking ownership of it
Create a reference with
&value— this act is called borrowingImmutable references (
&T) provide read-only access to dataMultiple immutable references to the same value can coexist safely
References do not own data — the original owner is unaffected
Rust prevents dangling references at compile time — always
Slices (
&str,&[T]) are reference types pointing into part of a collectionEvery reference has a lifetime that must fit within the owner's lifetime