Rust — Borrowing
Borrow instead of move
Passing ownership everywhere is exhausting. References let functions use values temporarily:
fn length(s: &String) -> usize { // & = "borrow this"
s.len()
} // borrowed value NOT dropped
fn main() {
let text = String::from("hello");
let len = length(&text); // lend it — ownership stays home
println!("'{}' is {} chars", text, len); // ✓ text still usable
}
&text creates a reference pointing at text without owning it. The function reads; the original survives.
Rule 1 — many shared borrows, OR…
Any number of read-only borrows may coexist:
let s = String::from("data");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{} {} {}", r1, r2, r3); // ✓ three readers, zero conflicts
Rule 2 — …but only ONE mutable borrow
Writing requires exclusivity:
let mut s = String::from("hi");
let w1 = &mut s;
// let w2 = &mut s; ❌ second mutable borrow — compile error!
w1.push_str(" there");
And a mutable borrow excludes ALL other borrows while alive:
let mut s = String::from("hi");
let r = &s;
let w = &mut s; // ❌ can't borrow as mutable while r exists
println!("{}", r);
Why these rules? They make data races and use-after-modify bugs impossible at compile time. The classic bug they prevent:
// JavaScript happily lets this produce garbage:
list.push(list[0]) // while iterating…
Non-lexical lifetimes — borrows end early
A borrow ends at its LAST USE, not at scope end:
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1); // ← last use of r1: borrow ENDS here
let w = &mut s; // ✓ fine now — nothing borrows anymore
w.push('!');
Older compilers scoped to the whole block; modern Rust (NLL) tracks actual usage. This is why code that "should" fail sometimes compiles.
Dangling references are impossible
fn dangle() -> &String { // ❌ compile error
let s = String::from("hi");
&s // s dies at end of fn → reference would dangle
}
The borrow checker refuses any reference outliving its data. C/C++ let this through; Rust makes it unrepresentable.
Slices — borrowed views into data
let s = String::from("hello world");
let hello = &s[0..5]; // &str — view of first 5 bytes
let world = &s[6..];
// string literals ARE slices:
let literal: &str = "already a slice";
Slices explain why APIs prefer &str: accepting &str accepts both owned strings (&s) and literals.
The idiomatic signature
// ✗ forces callers to own:
fn print(s: String) { … }
// ✓ accepts String AND &str literals:
fn print(s: &str) { … }
print(&my_string); // borrow of owned
print("literal"); // works directly
Fixing the top-3 borrow errors
| Error message | Usual fix |
|---|---|
| cannot borrow as mutable | add mut to variable + change reader borrows to end earlier |
| value borrowed after move | pass &value, or clone if truly needed |
| missing lifetime specifier | restructure so output clearly derives from inputs (lifetimes lesson) |
Mini Practice
- Rewrite consume() from the ownership lesson as borrow-based.
- Create three simultaneous &borrows; print all.
- Trigger double-mutable-borrow error; fix by scoping the first borrow.
- Write longest() taking two &str and returning the longer one.
- Change a
Stringparameter to&str; update call sites.
Next: arrays →
Related Topics
Frequently Asked Questions about Borrowing
What is Borrowing in Rust?
Borrowing is a fundamental concept in Rust. This lesson explains it step by step with clear examples, making it easy for beginners to understand.
How do I learn Borrowing?
Start by reading the explanation above, then try the code examples. Practice by modifying the examples and experimenting with different values. Hands-on practice is the best way to learn Borrowing.
Why is Borrowing important in Rust?
Borrowing is essential for Rust development. Understanding this concept will help you write better code and solve real-world problems more effectively.