Borrowing and References
21 min read
In TypeScript and JavaScript, you pass objects and arrays around by reference all day without thinking about it — and you accept the consequence that any function might quietly mutate your data. Rust gives you references too, but with a twist: the compiler tracks every borrow and guarantees, at compile time, that a reference can never outlive the data it points to.
Quick Overview
Section titled “Quick Overview”A reference (written &value) lets a function read or use a value without taking ownership of it — the original owner keeps the value and can use it again afterward. Rust’s borrow checker verifies, while compiling, that every reference points to live data, which is how Rust eliminates an entire category of bugs (dangling pointers, use-after-free) that you would otherwise debug at runtime.
This page covers shared (immutable) references — the &T borrow that lets you read. Mutable references (&mut T) and their exclusivity rule have their own page: see Mutable References.
TypeScript/JavaScript Example
Section titled “TypeScript/JavaScript Example”In JavaScript, objects and arrays are always handled through references. When you pass one to a function, the function receives a pointer to the same underlying object — so it can mutate your data, and the change is visible to you afterward.
// TypeScript/JavaScript - objects are passed by referenceinterface User { name: string; email: string; loginCount: number;}
function describe(user: User): string { return `${user.name} <${user.email}> logged in ${user.loginCount} times`;}
// A function can mutate the object you handed it...function recordLogin(user: User): void { user.loginCount += 1; // mutates the caller's object!}
const user: User = { name: "Ada", email: "ada@example.com", loginCount: 7 };
console.log(describe(user)); // read itrecordLogin(user); // and silently change itconsole.log(user.loginCount); // 8 — the original object changed
// Aliasing: two names, one objectconst alias = user;alias.loginCount = 0;console.log(user.loginCount); // 0 — `user` and `alias` ARE the same objectKey points:
- Objects/arrays are passed and assigned by reference.
- Any holder of the reference can mutate the shared object.
- There is no language-level distinction between “I want to read this” and “I want to change this.” You rely on convention, documentation, or defensive copies (
structuredClone, spreads) to stay safe.
Rust Equivalent
Section titled “Rust Equivalent”Rust makes the distinction explicit. A plain & reference is a read-only loan: the borrower may look at the value but not change it. The owner keeps the value the whole time.
#[derive(Debug)]struct User { name: String, email: String, login_count: u32,}
// `&User` is a SHARED reference: read-only access, no ownership taken.fn describe(user: &User) -> String { format!( "{} <{}> logged in {} times", user.name, user.email, user.login_count )}
fn is_active(user: &User) -> bool { user.login_count > 0}
fn main() { let user = User { name: String::from("Ada"), email: String::from("ada@example.com"), login_count: 7, };
// Borrow the same value as many times as we like. println!("{}", describe(&user)); println!("active? {}", is_active(&user));
// `user` is still fully owned here — nothing was moved or consumed. println!("{:?}", user);}Output (compile-verified):
Ada <ada@example.com> logged in 7 timesactive? trueUser { name: "Ada", email: "ada@example.com", login_count: 7 }Key points:
&usercreates a shared borrow;&Useris the type “shared reference to aUser.”- The function borrows the value, uses it, and gives it back —
useris usable afterward. - A shared reference cannot be used to mutate. To change the value, you would need a mutable reference (
&mut), covered on the next page.
Note: Without borrowing, passing
userintodescribewould move it (transfer ownership) and you could no longer useuserafterward. Borrowing is how you avoid that. See Ownership Rules and Move, Copy, Clone for the full story on moves.
Detailed Explanation
Section titled “Detailed Explanation”What a reference actually is
Section titled “What a reference actually is”A reference is a pointer: a small value that holds the memory address of something else. The & operator creates a reference; the * operator dereferences it (follows the pointer to read the value behind it).
fn main() { let x = 10; let r = &x; // r is a reference to x (a pointer under the hood)
println!("x = {}", x); println!("*r = {}", *r); // explicitly follow the pointer println!("r = {}", r); // most operations auto-deref, so *r is often optional
// A reference holds the address of what it points at. println!("address of x : {:p}", &x); println!("value held by r : {:p}", r); println!("equal? {}", std::ptr::eq(&x, r));}Output (compile-verified; the exact hex addresses vary by run and platform, but the two are always equal):
x = 10*r = 10r = 10address of x : 0x16d025c44value held by r : 0x16d025c44equal? trueThe two printed addresses are identical because r literally points at x. In most everyday code you rarely write * yourself — Rust auto-dereferences for method calls and field access (user.name works whether user is a User or a &User). You reach for * mainly when you need the value itself, such as in arithmetic (*r + 1).
Note: Unlike a JavaScript reference, a Rust reference is guaranteed non-null and always valid for as long as it exists. There is no
null, noundefined, and no dangling pointer — the compiler proves this before your program ever runs.
Borrowing instead of moving
Section titled “Borrowing instead of moving”Here is the canonical example. We borrow a String so the function can measure it without consuming it.
fn main() { let message = String::from("hello, borrow");
// Borrow `message` immutably — `len` only needs to read it. let length = calculate_length(&message);
// `message` is still valid here because we only lent it out. println!("'{}' has length {}", message, length);}
fn calculate_length(s: &String) -> usize { s.len()} // `s` (the reference) goes out of scope here, but because it is only a // borrow, the underlying String is NOT dropped — the caller still owns it.Output (compile-verified):
'hello, borrow' has length 13The action of creating a reference is called borrowing. The function calculate_length borrows message, reads its length, and when s goes out of scope only the reference is dropped — never the String it pointed at. The owner (main) gets its value back intact.
Note:
&Stringis used here purely to mirror the owned-vs-borrowed contrast against the moving version. In production you would writes: &str(see Best Practice #2) — andcargo clippywill in fact suggest exactly that via theclippy::ptr_arglint, since&straccepts both&Stringand string slices.
Many shared borrows at once are fine
Section titled “Many shared borrows at once are fine”A shared reference promises read-only access. Because no one can mutate, it is perfectly safe to hand out any number of shared references simultaneously:
fn main() { let data = vec![1, 2, 3, 4, 5];
let first = &data; let second = &data; let third = &data;
// Many simultaneous shared borrows are allowed: nobody can mutate. println!("sum via first = {}", first.iter().sum::<i32>()); println!("len via second = {}", second.len()); println!("max via third = {:?}", third.iter().max());
// The original owner is still usable too. println!("data = {:?}", data);}Output (compile-verified):
sum via first = 15len via second = 5max via third = Some(5)data = [1, 2, 3, 4, 5]This is the “shared” half of Rust’s central borrowing rule: either many shared (&) references, or exactly one mutable (&mut) reference, but never both at the same time. The exclusivity of &mut is the subject of the Mutable References page; here it is enough to know that as long as you only borrow with &, you can have as many borrows as you want.
The borrow checker
Section titled “The borrow checker”The borrow checker is the part of the Rust compiler that tracks the lifetime of every reference and rejects any program where a reference could point at data that is gone or is being mutated out from under it. It runs at compile time, so the checks cost nothing at runtime. The two guarantees most relevant to shared references are:
- A reference may never outlive the value it borrows (no dangling references).
- While any shared reference exists, the borrowed value cannot be mutated or moved.
You do not call the borrow checker; it is simply part of cargo build / cargo check. When it rejects your code you get an error like E0597 (“does not live long enough”) or E0502 (“cannot borrow as mutable because it is also borrowed as immutable”). Reading those errors is a core Rust skill — they almost always point at exactly the right fix.
Dangling references are impossible
Section titled “Dangling references are impossible”In C you can return a pointer to a local variable and get undefined behavior. In JavaScript you can’t even express the problem because the GC keeps anything reachable alive. Rust takes a third path: it makes the dangling case a compile error.
// does not compile (error[E0106]: missing lifetime specifier)fn dangle() -> &String { let s = String::from("temporary"); &s // returning a reference to `s`...} // ...but `s` is dropped here, so the reference would dangleThe compiler refuses this outright (see Common Pitfalls for the full message). The fix is to return the owned String instead of a reference to it — then ownership moves out to the caller and nothing is left dangling:
fn no_dangle() -> String { let s = String::from("temporary"); s // ownership moves out to the caller — perfectly safe}
fn main() { let owned = no_dangle(); println!("{}", owned);}Key Differences
Section titled “Key Differences”| Aspect | TypeScript/JavaScript | Rust |
|---|---|---|
| How objects are passed | Always by reference, implicitly | You choose: by value (move/copy) or by reference (& / &mut) |
| Read vs. write intent | Not expressed in the language | &T = read-only, &mut T = read/write |
| Can the callee mutate my data? | Yes, silently, unless you copy defensively | Only if you explicitly lend a &mut |
| Dangling references | Impossible (GC keeps things alive) | Impossible (compiler rejects them) — but without a garbage collector |
| Multiple holders mutating at once | Allowed (a common bug source) | Forbidden at compile time for &mut; unlimited for & |
| Cost | GC bookkeeping, allocation pressure | Zero runtime cost; checks happen at compile time |
null / undefined references | Possible (obj?.field) | A &T is never null; absence is modeled with Option<&T> |
The big mental shift
Section titled “The big mental shift”In JavaScript, “passing a reference” is the only option and it always grants full mutation rights to the callee. In Rust, references are explicit, typed, and read-only by default. A function signature like fn save(user: &User) is a machine-checked promise: “I will look at your User, I will not change it, and I will not keep it after I return.” That promise is enforced by the compiler, not by code review.
Tip: When migrating a mental model, read
&Tas “a temporary read-only loan of aT.” The owner is guaranteed to still have the value when the loan ends.
Borrowing vs. cloning
Section titled “Borrowing vs. cloning”A JavaScript habit is to defensively copy ({ ...obj }, structuredClone(obj)) to avoid accidental shared mutation. In Rust, borrowing usually replaces that copy entirely: you hand out a cheap & reference instead of duplicating data. Reach for .clone() only when you genuinely need a second independent owner — see Move, Copy, Clone.
Common Pitfalls
Section titled “Common Pitfalls”Pitfall 1: Returning a reference to a local value
Section titled “Pitfall 1: Returning a reference to a local value”This is the dangling-reference attempt. The borrowed value would be destroyed at the end of the function.
// does not compilefn dangle() -> &String { let s = String::from("temporary"); &s}
fn main() { let r = dangle(); println!("{}", r);}Real compiler error:
error[E0106]: missing lifetime specifier --> src/main.rs:2:16 |2 | 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 fromhelp: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static` |2 | fn dangle() -> &'static String { | +++++++help: instead, you are more likely to want to return an owned value |2 - fn dangle() -> &String {2 + fn dangle() -> String { |Fix: Return the owned String (drop the &). The compiler’s last suggestion is exactly right. When you genuinely need to return a borrow, it must borrow from one of the function’s inputs, which is what Lifetimes are about.
Pitfall 2: Using a value after its referent has been dropped
Section titled “Pitfall 2: Using a value after its referent has been dropped”A subtler version: the reference outlives the value’s scope.
// does not compilefn main() { let reference; { let value = String::from("short-lived"); reference = &value; } // `value` is dropped here println!("{}", reference); // tries to use the freed value}Real compiler error:
error[E0597]: `value` does not live long enough --> src/main.rs:6:21 |5 | let value = String::from("short-lived"); | ----- binding `value` declared here6 | reference = &value; | ^^^^^^ borrowed value does not live long enough7 | } // `value` is dropped here | - `value` dropped here while still borrowed8 | println!("{}", reference); // tries to use the freed value | --------- borrow later used hereFix: Make the borrowed value live at least as long as the reference — usually by declaring value in the outer scope. In a GC language this code would “work” by keeping value alive; Rust instead tells you the lifetimes don’t line up.
Pitfall 3: Passing by value when you meant to borrow
Section titled “Pitfall 3: Passing by value when you meant to borrow”A very common beginner mistake: forgetting the &, which moves the value into the function.
// does not compilefn print_it(s: String) { println!("{}", s);}
fn main() { let owned = String::from("data"); print_it(owned); // moves `owned` into the function println!("{}", owned); // try to use it again}Real compiler error (abridged):
error[E0382]: borrow of moved value: `owned` --> src/main.rs:9:20 |7 | let owned = String::from("data"); | ----- move occurs because `owned` has type `String`, which does not implement the `Copy` trait8 | print_it(owned); // moves `owned` into the function | ----- value moved here9 | println!("{}", owned); // try to use it again | ^^^^^ value borrowed here after move |note: consider changing this parameter type in function `print_it` to borrow instead if owning the value isn't necessaryhelp: consider cloning the value if the performance cost is acceptable |8 | print_it(owned.clone()); // moves `owned` into the function | ++++++++Fix: Borrow instead of moving — change the signature to fn print_it(s: &String) (or better, s: &str) and call it as print_it(&owned). Cloning also compiles, but it needlessly duplicates the data; prefer borrowing.
Pitfall 4: Trying to mutate through a shared reference
Section titled “Pitfall 4: Trying to mutate through a shared reference”A shared & reference is read-only. Attempting to write through it is a compile error.
// does not compilefn main() { let mut count = 0; let r = &count; // shared (immutable) reference *r += 1; // try to mutate through it println!("{}", count);}Real compiler error (abridged):
error[E0594]: cannot assign to `*r`, which is behind a `&` reference --> src/main.rs:4:5 |4 | *r += 1; // try to mutate through it | ^^^^^^^ `r` is a `&` reference, so the data it refers to cannot be written |help: consider changing this to be a mutable reference |3 | let r = &mut count; | +++Fix: Use &mut count to get a mutable reference (and note that count is already let mut). Mutable borrows have their own exclusivity rule — read Mutable References next.
Best Practices
Section titled “Best Practices”1. Borrow by default; take ownership only when you must
Section titled “1. Borrow by default; take ownership only when you must”If a function only needs to read a value, take it by shared reference. This is the most flexible and cheapest signature — callers keep their data and pay nothing for the call.
// Idiomatic: read-only access via a shared borrowfn word_count(text: &str) -> usize { text.split_whitespace().count()}2. Prefer &str over &String, and &[T] over &Vec<T>
Section titled “2. Prefer &str over &String, and &[T] over &Vec<T>”A &str accepts both string literals and borrowed Strings (via automatic deref coercion), so it is strictly more general. Likewise, &[T] accepts arrays, Vecs, and other slices.
fn shout(text: &str) -> String { text.to_uppercase()}
fn main() { let owned = String::from("hello"); println!("{}", shout(&owned)); // &String coerces to &str println!("{}", shout("world")); // &str literal works too}Tip: Clippy will actively suggest changing
&Stringparameters to&strand&Vec<T>to&[T]. Following this makes your APIs accept more callers for free.
3. Let the compiler guide you
Section titled “3. Let the compiler guide you”The borrow-checker errors are unusually good. When you hit one, read the help: lines — they frequently contain the literal fix (add &, add mut, return an owned value). Treat the borrow checker as a pair programmer, not an adversary.
4. Reach for references before .clone()
Section titled “4. Reach for references before .clone()”If you find yourself cloning to “make the error go away,” pause: a borrow is usually the right answer and avoids the allocation. Clone only when you truly need a second independent owner.
Real-World Example
Section titled “Real-World Example”A small order-processing module. Three functions all borrow a slice of orders; none takes ownership, so the caller can run every report in sequence on the same data without any copying.
use std::collections::HashMap;
#[derive(Debug)]struct Order { id: u32, customer: String, total_cents: u64, status: String,}
// Borrow the orders read-only; sum the paid ones.fn total_revenue(orders: &[Order]) -> u64 { orders .iter() .filter(|o| o.status == "paid") .map(|o| o.total_cents) .sum()}
// The returned map borrows customer names FROM `orders`, so its keys// (`&str`) live exactly as long as the borrowed slice. No strings copied.fn revenue_by_customer(orders: &[Order]) -> HashMap<&str, u64> { let mut totals: HashMap<&str, u64> = HashMap::new(); for order in orders { if order.status == "paid" { *totals.entry(order.customer.as_str()).or_insert(0) += order.total_cents; } } totals}
// Return a borrow of one order that lives as long as the input slice.fn find_order<'a>(orders: &'a [Order], id: u32) -> Option<&'a Order> { orders.iter().find(|o| o.id == id)}
fn main() { let orders = vec![ Order { id: 1, customer: String::from("Ada"), total_cents: 4_500, status: String::from("paid") }, Order { id: 2, customer: String::from("Linus"), total_cents: 9_900, status: String::from("refunded") }, Order { id: 3, customer: String::from("Ada"), total_cents: 1_200, status: String::from("paid") }, ];
println!("Total revenue: ${:.2}", total_revenue(&orders) as f64 / 100.0);
let by_customer = revenue_by_customer(&orders); let mut rows: Vec<_> = by_customer.iter().collect(); rows.sort(); for (customer, cents) in rows { println!(" {customer}: ${:.2}", *cents as f64 / 100.0); }
match find_order(&orders, 3) { Some(order) => println!("Found order {}: {} cents", order.id, order.total_cents), None => println!("No such order"), }
// `orders` is still fully owned and usable here. println!("Processed {} orders.", orders.len());}Output (compile-verified):
Total revenue: $57.00 Ada: $57.00Found order 3: 1200 centsProcessed 3 orders.Notice that revenue_by_customer returns a HashMap<&str, u64> whose keys are borrowed from the orders — the borrow checker guarantees the map cannot outlive the data it points into. And find_order uses an explicit lifetime ('a) to say “the returned reference lives as long as the orders slice you gave me.” That annotation is your bridge to the next topics: Lifetimes and Lifetime Elision.
Note:
cargo clippywill flagfind_order’s'awithclippy::needless_lifetimes, because the elision rules let you writefn find_order(orders: &[Order], id: u32) -> Option<&Order>and the compiler infers the same lifetime. The explicit'ais shown here on purpose, to make the “returned reference borrows from the input” relationship visible before we cover elision.
Further Reading
Section titled “Further Reading”Official Documentation
Section titled “Official Documentation”- The Rust Book — References and Borrowing
- The Rust Book — The Slice Type
- Rust by Example — Borrowing
- Rust Reference — References (
&and&mut)
Note: This guide targets the latest stable Rust (1.96.0) and the latest stable edition (2024). Everything here is verified against current-stable tooling;
cargo newselects the newest edition automatically.
Related Sections in This Guide
Section titled “Related Sections in This Guide”- Section 05 — Ownership (overview)
- Stack vs. Heap — where the value a reference points at actually lives
- Ownership Rules — the rules that make borrowing necessary
- Mutable References —
&mutand the one-writer-XOR-many-readers rule - Lifetimes — naming how long references stay valid
- Lifetime Elision — when you can omit lifetime annotations
- Move, Copy, Clone — what happens when you don’t borrow
- Reference Counting (
Rc/Arc) — shared ownership when a single owner isn’t enough - Variables and Mutability —
letvslet mut, the foundation for&vs&mut - Introduction and Getting Started — if you need a refresher on setup
- Section 06 — Data Structures — structs and enums you’ll be borrowing next
Exercises
Section titled “Exercises”Exercise 1: Borrow instead of move
Section titled “Exercise 1: Borrow instead of move”Difficulty: Beginner
Objective: Convert a function that consumes its argument into one that borrows it.
Instructions: The function below takes ownership of the Vec, so the call site can’t use names afterward. Rewrite longest_name to borrow the data so that main can still print names at the end. Implement the body so it returns a reference to the longest name.
fn longest_name(names: Vec<String>) -> String { // TODO: borrow instead of consuming; return a reference to the longest name /* ??? */}
fn main() { let names = vec![ String::from("Bo"), String::from("Alexander"), String::from("Kai"), ]; println!("Longest: {}", longest_name(/* ??? */)); println!("All names still here: {:?}", names); // must still compile}Solution
fn longest_name(names: &[String]) -> &String { let mut longest = &names[0]; for name in names { if name.len() > longest.len() { longest = name; } } longest}
fn main() { let names = vec![ String::from("Bo"), String::from("Alexander"), String::from("Kai"), ]; println!("Longest: {}", longest_name(&names)); println!("All names still here: {:?}", names);}Output:
Longest: AlexanderAll names still here: ["Bo", "Alexander", "Kai"]Taking &[String] (a slice) instead of Vec<String> borrows the data, so names remains owned by main. Returning &String hands back a reference into that borrowed slice.
Exercise 2: Return a borrowed slice
Section titled “Exercise 2: Return a borrowed slice”Difficulty: Intermediate
Objective: Write a function that returns a &str slice borrowed from its input.
Instructions: Implement first_word so it returns the first whitespace-delimited word of the input as a borrowed slice (no allocation, no String). It should accept both &String and &str literals.
fn first_word(s: &str) -> &str { // TODO: return everything up to the first space, or the whole string /* ??? */}
fn main() { let sentence = String::from("borrow checker rules"); println!("{}", first_word(&sentence)); // "borrow" println!("{}", first_word("hello world")); // "hello" println!("{}", sentence); // sentence still usable}Solution
fn first_word(s: &str) -> &str { match s.find(' ') { Some(i) => &s[..i], None => s, }}
fn main() { let sentence = String::from("borrow checker rules"); println!("{}", first_word(&sentence)); println!("{}", first_word("hello world")); println!("{}", sentence);}Output:
borrowhelloborrow checker rulesThe returned &str borrows from s, so it can never outlive the string it slices into — the borrow checker enforces that automatically. Accepting &str (rather than &String) lets the same function serve both a borrowed String and a string literal.
Exercise 3: Prove that shared borrows alias safely
Section titled “Exercise 3: Prove that shared borrows alias safely”Difficulty: Intermediate
Objective: Show that multiple shared references to one value can coexist, and that the value remains usable afterward.
Instructions: Given the Counter struct, write a function read_twice that takes two shared references to a Counter and returns the sum of their value fields. In main, call it with two borrows of the same counter, then print the counter to confirm it was never consumed.
#[derive(Debug)]struct Counter { value: i32,}
fn read_twice(/* ??? */) -> i32 { // TODO /* ??? */}
fn main() { let c = Counter { value: 21 }; println!("Doubled: {}", read_twice(/* ??? */)); println!("{:?}", c);}Solution
#[derive(Debug)]struct Counter { value: i32,}
fn read_twice(a: &Counter, b: &Counter) -> i32 { a.value + b.value}
fn main() { let c = Counter { value: 21 }; // Two shared borrows of the same value, at once — perfectly legal. println!("Doubled: {}", read_twice(&c, &c)); println!("{:?}", c);}Output:
Doubled: 42Counter { value: 21 }Passing &c twice creates two simultaneous shared borrows. Because neither can mutate, this is allowed — and c is still owned by main afterward. (Try changing one parameter to &mut Counter and calling read_twice(&mut c, &c): the borrow checker will reject it, which is the exclusivity rule covered in Mutable References.)