Understanding Rust's Borrow Checker Through a Longest‑Word Example
Learn how Rust’s borrow checker lets you return a reference to data without copying, using a longest‑word function as a concrete example.
09 May 2026, 05:18 UTC

When you need to extract a piece of data without copying it, you often reach for a slice or a view. In many languages this means trusting the runtime to keep the original alive, or paying the cost of a clone. Rust solves the same problem at compile time with its ownership and borrowing system. The borrow checker enforces that every reference is valid for the exact lifetime of the data it points to, preventing dangling pointers and data races without a garbage collector.
Ownership basics
Every value in Rust has a single owner. When the owner goes out of scope the value is dropped, which runs its destructor and frees any resources. References (&T) allow you to borrow a value without taking ownership. There are two kinds of references:
&T– shared (immutable) borrow, many can exist at once.&mut T– exclusive (mutable) borrow, only one can exist.
The borrow checker enforces the "aliasing XOR mutability" rule: you may have any number of shared references or exactly one exclusive reference, but never both simultaneously. This rule is checked at compile time, so no runtime overhead is needed.
Worked example: longest word without allocation
Consider a function that receives a string slice, finds the longest word, and returns a slice that points into the original string. No allocation or copying occurs; the returned lifetime is tied to the input.
// edition:2021
fn longest_word(s: &str) -> &str {
let mut best = "";
let mut best_len = 0;
for word in s.split_whitespace() {
if word.len() > best_len {
best = word;
best_len = word.len();
}
}
best
}
fn main() {
let sentence = "the quick brown fox";
let longest = longest_word(sentence);
println!("Longest word: {}", longest);
}
The function signature fn longest_word(s: &str) -> &str tells the compiler that the output reference lives at least as long as the input reference s. Because of lifetime elision, we do not need to write an explicit lifetime parameter; the compiler infers that the returned &str is tied to the lifetime of s. The function iterates over s.split_whitespace(), which yields temporary slices that also borrow from s. When a longer word is found, we simply reassign the best variable to that slice—no data is moved or copied.
To see the borrow checker in action, try to mutate the original string while holding the returned slice:
fn bad_example() {
let mut s = String::from("hello world");
let r = longest_word(&s); // shared borrow of s
s.push_str("!"); // mutable borrow while r is alive
println!("{}", r);
}
Compiling this yields an error similar to:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:5:5
|
4 | let r = longest_word(&s); // shared borrow of s
| ----------------- immutable borrow occurs here
5 | s.push_str("!"); // mutable borrow occurs here
| ^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
6 | println!("{}", r);
| - immutable borrow later used here
The compiler rejects the code because the mutable borrow (s.push_str) would invalidate the existing shared borrow (r). This guarantee prevents dangling references and data races without any runtime checks.
Trade‑offs and escape hatches
The ownership model eliminates a whole class of bugs, but it can feel restrictive when you need multiple mutable views or shared ownership across threads. Rust provides explicit, opt‑out tools for those cases:
clone()– creates an owned duplicate; cheap for small types, explicit for large ones.Rc– reference‑counted pointer for shared ownership within a single thread.Arc– atomic reference‑counted pointer for sharing across threads.RefCellandMutex– interior mutability patterns that move the borrow check to runtime, with a panic or block if the rules are violated.
Using these tools is intentional; they make the trade‑off visible in the code. If you find yourself reaching for clone() or Rc frequently, it often signals a design that could benefit from clearer ownership boundaries.
Actionable closing
Start by writing functions that accept and return references whenever possible. Let the borrow checker guide you toward designs where data has a single, clear owner and references are short‑lived. When the checker rejects your code, treat the error as a design clue: either adjust the lifetime structure or choose an appropriate escape hatch. Over time, fighting the borrow checker becomes less frequent and the resulting code gains the safety guarantees Rust is known for—without sacrificing performance.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.