Chapter 4 — References, Scopes, and Non-Lexical Lifetimes
Goal
Predict how long a borrow lasts and distinguish a reference's scope from the lifetime required for it to remain valid.
Prediction
fn main() {
let mut name = String::from("Ada");
let first = &name;
println!("{first}");
name.push_str(" Lovelace");
println!("{name}");
}
What happens on modern Rust?
A. It fails because first remains in scope until the closing brace.
B. It compiles because the borrow through first ends after its last use.
C. It fails because referenced strings can never be mutated.
D. It compiles only in release mode.
Reveal: read the evidence
Answer: B. On Rust 1.92, rustc accepts it, and it prints:
Ada
Ada Lovelace
The binding first remains in lexical scope, but the borrow does not need to remain active after println!("{first}"). This is a non-lexical lifetime: borrow checking follows relevant uses rather than blindly extending every borrow to the end of its enclosing block.
Build the model
A reference is only valid while the value it points to is alive. Rust checks that relationship at compile time:
fn main() {
let outside: &String;
{
let inside = String::from("temporary");
outside = &inside;
}
println!("{outside}");
}
The representative evidence is:
error[E0597]: `inside` does not live long enough
|
4 | let inside = String::from("temporary");
5 | outside = &inside;
| ^^^^^^^ borrowed value does not live long enough
6 | }
| - `inside` dropped here while still borrowed
7 | println!("{outside}");
| ------- borrow later used here
The problem is not that references dislike nested blocks. The owner inside is destroyed at the inner closing brace, but outside would be used later.
Working model: a borrow lasts from its creation through its last relevant use. The referenced value must stay alive for that entire period.
Two different ideas are easy to mix up:
- A scope is a region of source code where a binding can be named.
- A lifetime is the region during which a reference must be valid.
In the prediction, first is nameable until the end of main, but no later code uses it. Its required lifetime can therefore end earlier. In the failing nested example, outside is used after inside is dropped, so no shortening can make the reference valid.
A reference never extends an owner's life. Rust does not quietly keep inside allocated because outside points to it. Instead, it rejects the program.
Precise model: the borrow checker reasons over control flow and inferred regions. “Ends at last use” is a strong working shortcut, but branches and returned references can require a borrow along multiple paths. Lifetime annotations, when needed in later APIs, describe relationships among reference lifetimes; they do not prolong values or perform runtime cleanup.
Simplest fix
Move the owner into a scope at least as wide as every use of the reference:
fn main() {
let inside = String::from("lasting");
let outside = &inside;
println!("{outside}");
}
Ownership now outlives the borrow.
Tradeoff alternative: return ownership
If a helper creates the data, return the value rather than a reference to its local variable:
fn make_label() -> String {
String::from("temporary")
}
fn main() {
let label = make_label();
println!("{label}");
}
Returning String transfers ownership safely and is usually cheap because moving a String does not copy all its characters. The tradeoff is that the caller now owns cleanup. Trying to return &String here would require pointing at a local value that is destroyed when the function returns.
Mini challenge
Will this compile?
fn main() {
let mut items = vec![1, 2, 3];
let first = &items[0];
println!("{first}");
items.push(4);
}
A. Yes, because first is not used after push.
B. No, because vectors cannot be borrowed.
C. No, because any reference lasts to the end of the block.
D. It depends on vector capacity at runtime.
Answer
A. The shared borrow's last use precedes push, so non-lexical lifetime analysis ends it in time. Runtime capacity does not affect whether the program type-checks. If you printed first after push, compilation would fail because push may relocate the vector's elements.