02 · Lifetimes¶
The previous module showed that a reference can never outlive the data it points to — the compiler rejects dangling references before the program runs. Lifetimes are the mechanism that makes this checking possible: they're a way of naming "how long a reference is valid for" so the compiler can verify your code respects it, especially once references start flowing through function signatures and structs instead of staying inside one block.
Lifetimes are not runtime durations¶
This is the biggest misconception to clear up first: a lifetime annotation doesn't change how long anything lives, and it has zero runtime cost. It's purely a piece of information for the compiler — a label that lets it match up "this reference" with "the scope it's allowed to be valid in," the same way a type annotation doesn't change a value's bits, just what the compiler is allowed to check about it.
The problem lifetimes solve¶
fn longest(x: &str, y: &str) -> &str { // ERROR: missing lifetime specifier
if x.len() > y.len() { x } else { y }
}
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 function body is fine — the problem is the signature. The return
value is a reference, but the compiler has no way to know, just from
&str and &str, whether the returned reference's validity is tied to x
or to y. Without that information it can't check call sites for safety, so
it refuses to guess.
Fixing it with a lifetime parameter¶
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("hello");
let s2 = String::from("world!!");
let result = longest(&s1, &s2);
println!("longest: {result}"); // longest: world!!
}
'a (read "tick-a") is a generic lifetime parameter, declared in angle
brackets just like a type parameter. x: &'a str, y: &'a str, -> &'a str
says: "the returned reference is valid for exactly as long as both x and
y are valid" — in practice, the compiler picks the smaller of the two
input lifetimes as the constraint. This isn't extra restriction the
annotation adds; it's documenting a constraint that was always true of the
function body, so the compiler can enforce it at every call site.
Lifetimes prevent the caller from misusing the result¶
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long string is long");
let result;
{
let s2 = String::from("short");
result = longest(s1.as_str(), s2.as_str());
println!("longest: {result}"); // fine, s2 is still alive here
}
// println!("{result}");
// ERROR: `s2` does not live long enough -- `result` might still
// reference it, but s2 was dropped at the end of the inner block
}
This is exactly the payoff: the compiler used the 'a constraint to figure
out that result could be borrowed from s2, and since s2 dies at the
end of the inner block, using result afterward is rejected — a would-be
dangling reference caught before the program ever runs.
Lifetime elision: why you don't always write 'a¶
Most functions that take and return references don't need explicit lifetime annotations — the compiler applies three elision rules automatically:
- Each elided input reference gets its own lifetime parameter.
- If there's exactly one input lifetime, it's assigned to all elided output lifetimes.
- If one parameter is
&selfor&mut self, its lifetime is assigned to all elided output lifetimes (methods almost always tie the return value toself).
// No annotations needed -- rule 2 applies: one input reference,
// so the output borrows from it.
fn first_word(s: &str) -> &str {
match s.find(' ') {
Some(pos) => &s[..pos],
None => s,
}
}
fn main() {
let sentence = String::from("hello there world");
println!("{}", first_word(&sentence)); // hello
}
The longest function from earlier needed explicit 'a precisely because
it breaks rule 2 and 3: there are two input references, and neither is
&self, so the compiler has no default to fall back on — you have to state
the relationship yourself.
Lifetimes in structs¶
A struct that holds a reference must declare a lifetime parameter, because the struct can't outlive the data its field borrows.
struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn announce(&self) {
println!("Excerpt: {}", self.text);
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("no sentence found");
let excerpt = Excerpt { text: first_sentence };
excerpt.announce(); // Excerpt: Call me Ishmael
}
Excerpt<'a> can never outlive novel, because text borrows from it — the
compiler enforces that at every use of excerpt, the same way it enforces
plain reference lifetimes.
The 'static lifetime¶
'static means "valid for the entire duration of the program." String
literals are 'static because they're baked directly into the compiled
binary:
'static shows up a lot in error messages from beginners trying to satisfy
the borrow checker by slapping 'static on everything — resist that urge.
It's the right answer only when the data genuinely lives forever (string
literals, values leaked intentionally, or globals); using it to paper over a
lifetime error usually just moves the error somewhere else, or forces you to
.clone() data that didn't need cloning.
How It Actually Works¶
Lifetime parameters like 'a are not runtime values, types, or anything
that exists after compilation — they're purely a notation you use to tell
the borrow checker facts it can't infer on its own about relationships
between input and output reference lifetimes. fn longest<'a>(x: &'a str, y:
&'a str) -> &'a str doesn't mean "x and y must literally live the same
length of time" — it means "the returned reference's lifetime is the
intersection (the shorter) of whatever 'a gets instantiated to for this
call," and the caller's borrow checker then uses that contract to verify the
returned reference isn't used past when either input becomes invalid. This
is exactly the same MIR-level dataflow analysis from Module 1's borrowing
discussion, just needing an explicit annotation when the relationship spans
a function boundary the compiler can't see through automatically — inside a
single function body, lifetimes are always inferred, never written.
'static is the special case where that region is "the whole program" —
literally true for string literals because the compiler embeds their bytes
directly into the binary's read-only data section (.rodata), giving them
an address that's valid for the process's entire lifetime with no
allocation or deallocation ever happening. Slapping 'static on a reference
that doesn't actually point at program-lifetime data doesn't create that
guarantee — it just asserts something false to the compiler, which is why
the fix that "worked" is usually the borrow checker forcing you toward
'static by making you .clone() the data into an owned value instead
(trading a borrow for real ownership) rather than the annotation itself
solving anything.
Cheat sheet¶
| Syntax | Meaning |
|---|---|
&'a str |
A reference valid for lifetime 'a |
fn f<'a>(x: &'a str) -> &'a str |
Output lifetime tied to input x |
struct S<'a> { field: &'a str } |
Struct can't outlive the data field borrows |
'static |
Valid for the whole program (string literals, leaked/global data) |
| Elision rule 1 | Each elided input reference gets its own lifetime |
| Elision rule 2 | One input lifetime → applied to all elided outputs |
| Elision rule 3 | &self/&mut self lifetime → applied to all elided outputs |
Exercise¶
Write a struct struct Highlight<'a> { word: &'a str, context: &'a str }
with a method fn show(&self) that prints "{word}" found in: {context}.
Then write a free function
fn find_highlight<'a>(context: &'a str, word: &'a str) -> Option<Highlight<'a>>
that returns Some(Highlight { word, context }) if context.contains(word)
is true, otherwise None. In main, call it with a sentence and a word that
appears in it, print the result with .show(), and add a comment explaining
which elision rule (if any) would have let you omit the lifetimes if this
were a method on context instead of a free function.