9.5Dangling references
Rule 2, "references must always be valid," has been waiting patiently since lesson 9.1 while rule 1 got three lessons of attention. Today it takes the stage, and it earns it: this rule is the one Rust is famous for, and the bug it kills is best introduced the way the C++ world experiences it.
The horror story
A reference that outlives the value it points to is called a dangling reference: the window is still there, but the bicycle is gone, and what's behind the glass now is whatever happened to move in. In C++, the classic way to make one is heartbreakingly natural. Write a function that builds a local value and returns a reference to it. The local dies at the function's closing brace, as locals do (lesson 8.3), and the returned reference now points at reclaimed memory.
Here's the truly nasty part: that C++ program compiles, and it often works. The dead value's bytes are usually still sitting in the abandoned stack frame, undisturbed, so the program reads them and gets the right answer. Until the next function call overwrites that frame. Then the reference reads garbage, or crashes, or reads something secret, depending on the day. learncpp's chapter on this teaches the discipline of never returning a reference to a local; the language itself just trusts you to remember, every time, forever.
The same program in Rust
fn dangle() -> &String {
let s = String::from("hello");
&s
}
fn main() {
let view = dangle();
println!("{view}");
}
The logic of the bug is identical: s is dropped at dangle's closing brace (chapter 8 made sure you'd know that), so the returned reference would point at a freed heap block. The outcome is not identical:
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 from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
1 | fn dangle() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
1 - fn dangle() -> &String {
1 + fn dangle() -> String {
|
This transcript needs a slower read than usual, because the headline isn't about dangling at all. The compiler is pointing at the signature, before it has even judged the body. fn dangle() -> &String promises to return a borrowed value, and the compiler immediately asks the only question that matters about a borrow: borrowed from what? A reference's validity depends on how long the value behind it lives, and a function signature must carry enough information to answer that. This one can't. There's no parameter the result could be borrowed from, and no way for any caller to keep the mystery value alive.
The first = help line is the plain-words version: "there is no value for it to be borrowed from." The first suggestion (&'static) names a special case, a reference to something that lives for the entire program; hold that thought for two lessons. And the second suggestion is the actual fix, stated like the compiler has read chapter 5: you are more likely to want to return an owned value.
fn no_dangle() -> String {
String::from("hello")
}
Ownership moves out to the caller (lesson 8.6), nothing dangles, and nothing extra is copied: the move hands over the same handle. Back in lesson 5.x, the design notes said read_name returns an owned String because "a view of the function's local input wouldn't survive," and promised the intuition would become a compiler-enforced law. This transcript is that law.
The word lifetime
The headline used a term we owe a definition: a lifetime is the stretch of the program during which a value is alive, from creation to drop. Every value has one, every reference is only valid while its value's lifetime lasts, and what the compiler wanted from dangle's signature was a named lifetime connecting the output to something that outlives the call.
How to write those names ('static was your first glimpse, and the spelling &'static str photobombed a quiz back in lesson 4.7) is chapter 17's business, and it can comfortably wait. Here's why: in the overwhelming majority of functions, the compiler works lifetimes out by itself, and everything you write between now and chapter 17 runs on intuition plus the two rules. The intuition is one sentence long: a function can return a reference only if it borrows from something that lives outside the function.
The shape that works
That sentence has a happy case, and you've been using it since chapter 5. A function can return a reference into one of its own parameters, because the parameter borrows from the caller's value, which is alive on both sides of the call:
fn trimmed(text: &str) -> &str {
text.trim()
}
fn main() {
let raw = String::from(" Ada \n");
let clean = trimmed(&raw);
println!("[{clean}]");
}[Ada]
The returned view points into raw, which main owns and keeps alive; nothing dangles. This is exactly what trim itself does (lesson 5.4: a view into the same text, nudged and shortened), and the borrow checker connects the dots: clean counts as a live borrow of raw, so the rule-1 machinery from lesson 9.3 protects it. Try to raw.push_str(...) while clean is alive and you'll get patient 2's E0502.
Key insight
Both rules are now load-bearing and they interlock. Rule 2 says a reference must never outlive its value. Rule 1's one-writer clause is what keeps a valid reference truthful in the meantime. Together they make the promise from lesson 5.4 literal: programs where a view lies to you do not compile.
Quiz time
Question #1
Without the compiler, predict which of these compile. For each failure, say what's wrong in lifetime words.
a)
fn loudest() -> &String {
let cheer = String::from("GOAL");
&cheer
}
b)
fn loudest(cheer: &str) -> &str {
cheer.trim()
}
c)
fn loudest() -> String {
String::from("GOAL")
}Show solution
a) Refused (E0106). The function promises a borrowed return value, but there's nothing it could be borrowed from that outlives the call: cheer is dropped at the closing brace, so the reference would dangle.
b) Compiles. The returned view borrows from the parameter, and the parameter borrows from a value in the caller, which is alive after the call returns.
c) Compiles. No reference, no lifetime question: ownership of the String moves out to the caller.
Question #2
A C++ programmer tells you: "My version of program (a) compiles and prints GOAL just fine." Both of you are telling the truth. Explain to them what their compiler and yours each did.
Show solution
Their compiler accepted a function that returns a reference to a local, and the program appears to work because the dead value's bytes are usually still in the abandoned stack frame when they're read; nothing has overwritten them yet. It's undefined behavior wearing a "works on my machine" sticker, and it will eventually print garbage or worse, likely after some unrelated change. The Rust compiler refused to compile the same logic, because the signature couldn't name anything the returned reference validly borrows from. One language found the bug at compile time; the other scheduled it for production.
Rule 2 is proven, the chapter's machinery is complete, and one promissory note is left in the drawer: lesson 5.4 said carving a view of part of a string "rides on machinery from chapter 9." The machinery is now installed. Next lesson, we carve.