Rust Lifetimes
Why Lifetimes Exist
// This does NOT compile:
fn dangling() -> &String {
let s = String::from("hello");
&s // ERROR: s is dropped, reference would be dangling
}
// Return owned data instead:
fn not_dangling() -> String {
String::from("hello") // ownership moves to caller
}
Lifetimes are the compiler’s way of ensuring every reference is valid for as long as it is used. Most of the time the compiler infers them automatically.
Lifetime Annotations
// Without annotation — compiler cannot determine which input
// the return reference relates to
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");
let result;
{
let s2 = String::from("xyz");
result = longest(&s1, &s2);
println!("{result}"); // OK — s2 still alive
}
// println!("{result}"); // ERROR if uncommented: s2 is dropped
}
'a does not change how long values live. It tells the compiler: "the returned reference lives at most as long as the shortest of x and y." The compiler enforces this constraint at every call site.
Lifetime Elision Rules
// Rule 1: Each reference parameter gets its own lifetime
// fn foo(x: &str, y: &str) → fn foo<'a, 'b>(x: &'a str, y: &'b str)
// Rule 2: One input reference → output gets the same lifetime
// fn first_word(s: &str) -> &str → fn first_word<'a>(s: &'a str) -> &'a str
// Rule 3: &self or &mut self → output gets self's lifetime
// fn name(&self) -> &str → fn name<'a>(&'a self) -> &'a str
These three rules handle most cases. You only need explicit annotations when the compiler cannot determine which input lifetime applies to the output.
Lifetimes in Structs
struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn first_sentence(&self) -> &str {
self.text.split('.').next().unwrap_or(self.text)
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let excerpt = Excerpt {
text: &novel[..],
};
println!("{}", excerpt.first_sentence());
}
// excerpt cannot outlive novel — the compiler enforces this
A struct with &'a str borrows data from somewhere. The struct instance cannot outlive the data it references.
Static Lifetime
// String literals are 'static — baked into the binary
let s: &'static str = "I live forever";
// Owned types satisfy 'static bounds (they have no references)
fn spawn_thread(name: String) {
std::thread::spawn(move || {
println!("{name}");
});
// String is 'static because it owns its data
}
// T: 'static means T contains no non-static references
// It does NOT mean T lives forever — it means T can live as long as needed
fn requires_static<T: 'static>(val: T) { }
'static is the longest possible lifetime. String literals have it. T: 'static is a bound that means "T has no short-lived borrows" — not "T lives forever."
Common Patterns
// Different lifetimes — return relates only to first parameter
fn first_or_default<'a>(items: &'a [&str], default: &str) -> &'a str {
items.first().copied().unwrap_or(default)
// ERROR: default has different lifetime
}
// Fix: same lifetime for both
fn first_or_default<'a>(items: &'a [&'a str], default: &'a str) -> &'a str {
items.first().copied().unwrap_or(default)
}
When multiple references contribute to the return value, they need compatible lifetimes. The compiler’s error messages guide you to the correct annotations.