Lifetimes
Lifetimes are the compiler’s way of tracking how long references are valid. They prevent dangling references without a garbage collector. Most of the time you never write them - the compiler infers them. But understanding lifetimes is essential for structs that hold references and complex function signatures.
What Lifetimes Are
A lifetime is a scope during which a reference is valid. The compiler uses lifetimes to guarantee that references never outlive the data they point to.
fn main() {
let r; // r has lifetime 'a
{
let x = 5; // x has lifetime 'b (shorter)
r = &x; // Error: x doesn't live long enough
} // x is dropped here
// println!("{r}"); // r would be a dangling reference
}
The compiler sees that r references x, but x is dropped before r is used. This is a compile-time error, not a runtime crash.
Lifetime Elision Rules
The compiler infers lifetimes in common cases so you don’t have to write them. Three rules handle most functions:
- Each reference parameter gets its own lifetime
- If there’s exactly one input lifetime, it’s assigned to all output references
- If one parameter is
&selfor&mut self, the self lifetime is assigned to all outputs
// These are equivalent - the compiler adds the lifetime annotations:
fn first_word(s: &str) -> &str { /* ... */ }
// becomes:
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ }
// Method with &self - output gets self's lifetime:
fn name(&self) -> &str { /* ... */ }
// becomes:
fn name<'a>(&'a self) -> &'a str { /* ... */ }
Tip: You only need to write explicit lifetimes when the compiler can’t figure it out - typically functions with multiple reference parameters where it’s ambiguous which input lifetime the output should use.
Named Lifetimes
When the compiler can’t infer, you annotate with 'a syntax.
// The compiler can't tell which input the output borrows from.
// We annotate to say: the result lives as long as BOTH inputs.
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("hi");
result = longest(&s1, &s2);
println!("{result}"); // OK: both s1 and s2 are still alive
}
// println!("{result}"); // Error: s2 is dropped, result might reference it
}
Multiple lifetimes
When inputs have different lifetimes:
// The result borrows from the first argument only
fn first_from_pair<'a, 'b>(first: &'a str, _second: &'b str) -> &'a str {
first
}
Structs Holding References
When a struct stores a reference, it needs a lifetime parameter. This tells the compiler: “this struct can’t outlive the data it references.”
struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn new(text: &'a str) -> Self {
Excerpt { text }
}
fn words(&self) -> Vec<&str> {
self.text.split_whitespace().collect()
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = Excerpt::new(first_sentence);
println!("{}", excerpt.text); // OK: novel is still alive
}
Gotcha: Structs with lifetime parameters are a signal that borrowing relationships are getting complex. If a struct has more than one or two lifetime parameters, consider owning the data instead (
Stringvs&str,Vec<T>vs&[T]).
Common Patterns
Return owned data to avoid lifetime complexity
// Complex: lifetime annotations, caller must keep source alive
fn extract_domain<'a>(email: &'a str) -> &'a str {
email.split('@').nth(1).unwrap_or("")
}
// Simple: returns owned String, no lifetime concerns
fn extract_domain(email: &str) -> String {
email.split('@').nth(1).unwrap_or("").to_string()
}
The owned version is slightly less efficient (allocates), but much simpler. Choose owned data unless performance demands otherwise.
Structs that own or borrow with Cow
use std::borrow::Cow;
struct Config<'a> {
name: Cow<'a, str>, // can be borrowed &str OR owned String
}
impl<'a> Config<'a> {
fn new(name: &'a str) -> Self {
Config { name: Cow::Borrowed(name) }
}
fn with_prefix(name: &str) -> Config<'static> {
Config { name: Cow::Owned(format!("prefix-{name}")) }
}
}
Parse and return references into the input
struct Header<'a> {
name: &'a str,
value: &'a str,
}
fn parse_header(line: &str) -> Option<Header<'_>> {
let (name, value) = line.split_once(':')?;
Some(Header {
name: name.trim(),
value: value.trim(),
})
}
Tip: The
'_lifetime is an anonymous lifetime that tells the compiler “figure it out.” It’s useful in return positions to avoid naming a lifetime you only use once.
The 'static Lifetime
'static means the reference is valid for the entire program duration.
// String literals are always 'static
let s: &'static str = "hello";
// Owned data can be leaked to become 'static (rarely needed)
let leaked: &'static str = Box::leak(String::from("forever").into_boxed_str());
Common places you see 'static:
// Thread spawning requires 'static (data must outlive the thread)
std::thread::spawn(move || {
// captured variables must be owned or 'static
});
// Error trait bounds often require 'static
fn process() -> Result<(), Box<dyn std::error::Error + 'static>> {
Ok(())
}
Gotcha:
T: 'staticdoesn’t mean the value lives forever. It means the type doesn’t contain any non-static references.Stringis'static(it owns its data).&'a stris only'staticwhen'a = 'static.
When You Hit Lifetime Walls
| Symptom | Solution |
|---|---|
| ”does not live long enough” | Move data into the struct (own it) instead of borrowing |
| Too many lifetime parameters | Use owned types (String, Vec) instead of references |
| Self-referential struct | Use indices or Pin<Box<T>>, or redesign the data model |
| ”cannot return reference to local variable” | Return an owned value instead |
| Complex lifetime relationships | Consider Rc<str> or Arc<str> for shared ownership |
// Self-referential won't work:
struct Bad {
data: String,
slice: &str, // can't reference data - no way to express this lifetime
}
// Use an index instead:
struct Good {
data: String,
start: usize,
end: usize,
}
impl Good {
fn slice(&self) -> &str {
&self.data[self.start..self.end]
}
}