Lifetimes
Annotate how long references stay valid — making the borrow checker's reasoning explicit when it can't infer on its own.
- Explain why lifetimes exist and what problem they solve
- Write lifetime annotations using the 'a syntax
- Apply lifetime elision rules to know when annotations are unnecessary
- Add lifetime parameters to structs that hold references
- Understand 'static and when it legitimately appears
Every reference in Rust has a lifetime — the span of code during which the reference is valid. Most of the time the compiler can figure this out on its own through a process called lifetime elision. But sometimes, when a function takes or returns multiple references, the compiler can't tell which input a returned reference is tied to. That's when you annotate explicitly.
Lifetimes don't change how long values live. They're annotations that let you communicate the relationship between reference lifetimes to the compiler — and let it check your reasoning.
Why lifetimes exist
The core problem is the dangling reference. You saw in the previous lessons that the compiler prevents a function from returning a reference to a local variable. Lifetimes are the mechanism that makes that check possible for any function, including ones you write.
Consider a function that returns one of its two arguments:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}The compiler rejects this. The returned reference is either x or y, but the compiler can't know which at compile time — and the caller needs to know, so it can tell whether the return value outlives both inputs or just one. Without an annotation, the compiler can't verify safety.
The 'a annotation syntax
A lifetime parameter is written 'a (read "tick a"). It's a name for a lifetime region:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}'a here means: "the returned reference lives at most as long as the shorter of x and y." You're not saying anything about actual durations — you're describing a relationship. The borrow checker uses that to verify callers don't use the returned reference after either input is dropped.
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("xy");
result = longest(&s1, &s2);
println!("{}", result); // fine — result used inside s2's scope
}
// println!("{}", result); // ERROR — s2 is dropped, result may point to it
}Lifetime annotations don't make references live longer. They tell the compiler what relationship already exists so it can enforce it. Think of them as constraints you're declaring, not allocations you're making.
Lifetime elision rules
The compiler applies three rules before asking for annotations. If all three together determine every lifetime, no annotation is needed:
- Each reference parameter gets its own lifetime parameter.
- If there is exactly one input lifetime, it is assigned to all output lifetimes.
- If one of the inputs is
&selfor&mut self, its lifetime is assigned to all output lifetimes.
This is why most functions don't need explicit lifetimes. A function like fn first_word(s: &str) -> &str compiles fine because rule 1 gives the input 'a, and rule 2 assigns 'a to the output — unambiguously.
// You write:
fn first_word(s: &str) -> &str { ... }
// Compiler expands to:
fn first_word<'a>(s: &'a str) -> &'a str { ... }Lifetimes in structs
If a struct holds a reference, it must declare a lifetime parameter — otherwise the compiler can't ensure the struct doesn't outlive the data it refers to:
struct Excerpt<'a> {
text: &'a str,
}
fn main() {
let novel = String::from("Call me Ferris. Some years ago...");
let first_sentence = novel.split('.').next().expect("no sentence");
let excerpt = Excerpt { text: first_sentence };
println!("{}", excerpt.text);
}Excerpt<'a> says: "an Excerpt cannot outlive the string it borrows." If you try to drop novel while excerpt is still in scope, the compiler catches it.
'static lifetime
'static means the reference is valid for the entire lifetime of the program. String literals are 'static — their text is compiled into the binary:
let s: &'static str = "I am immortal";You'll also see 'static in trait bounds: T: 'static means T contains no non-'static references (it can be stored indefinitely). This comes up often with threads, which need to own their data or have 'static references.
Reaching for 'static to "fix" a lifetime error is usually wrong — it just shifts where the error appears. The real fix is usually to restructure so the owned data lives long enough, or to pass ownership instead of borrowing.
Check your understanding
Knowledge check
- 1.What is the primary purpose of lifetime annotations in Rust?
- 2.A function with a single &str input and a &str output always needs an explicit lifetime annotation.
- 3.Why do string literals have the 'static lifetime?
- 4.Why must a struct that contains a &str field declare a lifetime parameter?
Do it yourself
Write a struct Config<'a> that holds a &'a str for a name and a &'a str for a value. Write a method display(&self) that prints both. Try creating a Config that borrows from a String, then dropping the String while Config is still in scope — observe the compiler error.
struct Config<'a> {
name: &'a str,
value: &'a str,
}
impl<'a> Config<'a> {
fn display(&self) {
println!("{} = {}", self.name, self.value);
}
}Where to go next
Lifetimes round out the ownership picture. The next lesson moves to error handling with Result<T, E> — the idiomatic way Rust propagates failures without exceptions.