Ownership and Lifetimes, Advanced
How the borrow checker knows a reference is still valid, and the explicit lifetime syntax you need once it can't figure that out alone.
3 min read
Earlier in this course, borrowing let you use a value without owning it, and the compiler rejected references that could outlive the data they point to. What wasn't shown is how the compiler decides that — through lifetimes, and the rare cases where you have to spell one out yourself.
Every reference has a lifetime
A lifetime is simply the scope for which a reference is valid. Most of the time, the compiler infers lifetimes automatically (a process called lifetime elision) and you never write the syntax at all. It becomes visible when the compiler genuinely can't work out the relationship on its own.
The example that forces the issue
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}This fails to compile:
error[E0106]: missing lifetime specifier
= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `x` or `y`
The compiler isn't confused about whether x and y are valid — it's refusing to guess which one the returned reference's validity depends on. Depending on which branch runs, the return value borrows from x or from y, and the caller needs a guarantee covering whichever one it turns out to be.
Annotating a shared lifetime
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}'a is a lifetime parameter, read "tick-a" — a generic parameter for lifetimes, in the same spirit as T is a generic parameter for types. This signature says: the returned reference is valid for exactly as long as both x and y are valid — specifically, no longer than the shorter of the two. The annotation doesn't change how long anything actually lives; it describes a constraint that already exists in the code, so the compiler can verify callers respect it.
What this catches
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("short");
result = longest(s1.as_str(), s2.as_str());
} // s2 dropped here
println!("{result}"); // error: `s2` does not live long enough
}Because longest's signature promises the return value is only valid as long as both inputs are, and s2 is dropped at the end of the inner block, the compiler correctly refuses to let result be used afterward — it might be a dangling reference to s2. This is the same class of bug the basic borrowing lesson covered, just surfacing through a function boundary instead of directly in one scope.
Lifetimes on structs
A struct holding a reference needs a lifetime parameter too, so the compiler can guarantee the struct never outlives the data it points to:
struct Excerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = Excerpt { part: first_sentence };
println!("{}", excerpt.part);
}Excerpt<'a> cannot outlive the string slice it borrows — if novel were dropped while excerpt was still around, the compiler would reject it, the same way it rejected the dangling reference earlier in this course.
When you actually need to write this
In practice, most functions never need explicit lifetime annotations — the elision rules (roughly: one input reference means the output borrows from it; &self present means the output borrows from self) cover the overwhelming majority of real code. Explicit lifetimes show up mainly in structs that hold borrowed data, and in functions accepting multiple reference parameters whose relationship the compiler can't infer, exactly like longest above.
The reward for this extra syntax is the same one ownership and borrowing gave you earlier: dangling references, use-after-free, and a whole class of memory bugs that are notoriously hard to track down in C or C++ are instead caught here, at compile time, before the program ever runs.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.