Why this matters
A dangling reference is a pointer to memory that has already been freed or reused. Following it reads garbage or corrupts someone else's data. In C, returning a pointer to a local variable is one of the most common beginner bugs, and it often seems to work, until it does not. Rust makes dangling references impossible in safe code, with one rule: a reference must never outlive the value it points to. This lesson shows the errors you get when code would break that rule and how to fix each one. It also introduces the word lifetime, which module 10 develops fully.
The bug, in C terms
Imagine a function that builds a label and returns a pointer to it:
fn make_label() -> &String { main's frame make_label's frame
let s = String::from("hi"); +----------------+
&s | s: ptr ----------> [h|i]
} +----------------+
label ----------> (the frame above)
after make_label returns:
- s is dropped: the heap buffer [h|i] is freed
- make_label's frame is popped; the next function call reuses that stack memory
- label points at a dead stack slot that pointed at a freed buffer
Two layers of invalid memory. In C this compiles. In Rust it does not.
E0106 and E0515: returning a reference to a local
fn make_label() -> &String {
let s = String::from("hi");
&s
}
fn main() {
let label = make_label();
println!("{label}");
}
error[E0106]: missing lifetime specifier
--> main.rs:1:20
|
1 | fn make_label() -> &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 make_label() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
1 - fn make_label() -> &String {
1 + fn make_label() -> String {
|
error[E0515]: cannot return reference to local variable `s`
--> main.rs:3:5
|
3 | &s
| ^^ returns a reference to data owned by the current function
Two errors. The first, E0106 "missing lifetime specifier", talks about lifetimes, but the key sentence is "this function's return type contains a borrowed value, but there is no value for it to be borrowed from". A returned reference must point at something that existed before the call (passed in by the caller) or that lives for the whole program. A function with no reference parameters has nothing to borrow from. (The 'static suggestion would only be right for string literals and similar; it is not the fix here.)
If you try to satisfy the compiler by writing a lifetime, it catches the real problem anyway:
fn make_label() -> &'static String {
let s = String::from("hi");
&s
}
fn main() {
println!("{}", make_label());
}
error[E0515]: cannot return reference to local variable `s`
--> main.rs:3:5
|
3 | &s
| ^^ returns a reference to data owned by the current function
Only the second error, E0515 "cannot return reference to local variable", remains, and it names the real problem. The fix in both cases is to return the owned value:
fn make_label() -> String {
let s = String::from("hi");
s
}
fn main() {
let label = make_label();
println!("{label}");
}
hi
Ownership moves to the caller; nothing dangles. A good rule of thumb: a function that creates new data returns it owned. Functions return references only when they point into data the caller passed in.
Returning a reference into a parameter: fine
fn first_word(s: &str) -> &str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
fn main() {
let sentence = String::from("hello brave world");
let w = first_word(&sentence);
println!("{w}");
}
hello
The returned &str points into sentence's buffer, which the caller owns and which outlives the call. (s.find(' ') returns the byte position of the first space, or None; &s[..i] is a slice of the first i bytes, covered in the next lesson.) The compiler infers that the result borrows from the parameter, because there is only one parameter it could borrow from. That inference is called lifetime elision.
And the compiler keeps checking at the call site: as long as w is used, sentence may not be modified or dropped.
fn first_word(s: &str) -> &str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
fn main() {
let mut sentence = String::from("hello brave world");
let w = first_word(&sentence);
sentence.clear();
println!("{w}");
}
error[E0502]: cannot borrow `sentence` as mutable because it is also borrowed as immutable
--> main.rs:11:5
|
10 | let w = first_word(&sentence);
| --------- immutable borrow occurs here
11 | sentence.clear();
| ^^^^^^^^^^^^^^^^ mutable borrow occurs here
12 | println!("{w}");
| --- immutable borrow later used here
clear would empty the buffer w points into. The borrow carried through the function call, so the checker knows w depends on sentence.
E0597: the owner dies first
The same rule applies within one function, when a value goes out of scope before a reference to it is used:
fn main() {
let r;
{
let s = String::from("short-lived");
r = &s;
}
println!("{r}");
}
error[E0597]: `s` does not live long enough
--> main.rs:5:13
|
4 | let s = String::from("short-lived");
| - binding `s` declared here
5 | r = &s;
| ^^ borrowed value does not live long enough
6 | }
| - `s` dropped here while still borrowed
7 | println!("{r}");
| --- borrow later used here
E0597 "s does not live long enough", with labels showing where s is dropped and where r is used afterwards. Fixes: make the owner live longer (declare it in the outer scope), or make r an owned value (let r = s; moves the String out of the block instead of borrowing it).
Lifetimes: the idea
All three errors are about the same thing. Every reference has a lifetime: the span of the program during which it is valid to use. Every value has a span during which it exists, from creation to drop. The rule:
value s exists: |====================| (created ... dropped)
reference r used: |=========| OK: inside s's span
reference r used: |=======================| ERROR: outlives s
Within a function, the compiler works the spans out itself, which is what the previous lessons relied on. At function boundaries, it needs to know how the returned reference relates to the parameters. With exactly one reference parameter, it assumes "the result lives as long as that parameter" (elision). With several, or when storing references inside structs, you will write that relationship down with lifetime annotations like 'a. That is module 10. For now, the practical advice:
- Return owned values (
String,Vec) from functions that create data. - Return references only into data that was passed in.
- If the compiler asks for lifetime annotations and you are not sure, returning an owned value is almost always a correct (if sometimes slightly slower) alternative.
Common mistakes
- Adding
'staticto make E0106 go away. That promises the reference is valid for the entire program; the compiler will then reject the local reference with E0515. Return an owned value instead. - Borrowing from a temporary.
let r = &String::from("x");works (Rust extends the temporary's life to the enclosing block), butlet w = first_word(&String::from("a b"));followed by a later use ofwfails with E0716 "temporary value dropped while borrowed". Store the String in a variable first. - Expecting a reference to keep its owner alive. References never extend the life of what they point to. The owner decides; the reference must fit inside.
Recap
- A dangling reference points at freed or reused memory; safe Rust forbids them at compile time.
- E0106/E0515: a function cannot return a reference to its own local; return the owned value instead.
- Returning a reference into a parameter is fine; the borrow then extends to the caller's use.
- E0597: a value is dropped while a reference to it is still used later; extend the owner's scope or own the value.
- Every reference has a lifetime that must fit inside its value's life; module 10 shows how to name lifetimes.
Going further: read rustc --explain E0106 and rustc --explain E0597. Try writing fn longer(a: &str, b: &str) -> &str and read the error: it is the doorway to module 10.