Lifetimes in Rust
Understand lifetime annotations, the 'static lifetime, lifetime elision, and lifetimes in structs.
What Are Lifetimes?
Every reference in Rust has a lifetime — the scope for which the reference is valid. Most of the time the compiler infers lifetimes automatically. You only need to write explicit lifetime annotations when the compiler cannot figure out the relationship between the lifetimes of multiple references on its own.
Lifetimes prevent dangling references:
fn main() {
let r;
{
let x = 5;
r = &x; // ERROR: x does not live long enough
}
// println!("{}", r); // r would point to freed memory
}
The compiler sees that x is dropped before r is used and rejects the code.
Lifetime Annotation Syntax
Lifetime parameters are written with a leading apostrophe: 'a, 'b, 'input, etc. They appear after & in reference types and in angle brackets on function/struct signatures.
// Without annotation (may not compile for multi-reference returns)
// fn longest(x: &str, y: &str) -> &str { ... }
// With explicit lifetime annotation
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.as_str(), s2.as_str());
println!("longest: {}", result); // OK — result used inside s2's scope
}
}
The annotation 'a says: “the returned reference lives at least as long as the shorter of x and y’s lifetimes.” The compiler uses this to reject code where the return value would outlive one of the inputs.
Lifetime Elision
Rust has three elision rules that let the compiler infer lifetimes without explicit annotations in common cases:
Rule 1: Each reference parameter gets its own lifetime.
fn foo(x: &str) -> &str // expands to: fn foo<'a>(x: &'a str) -> &'a str
Rule 2: If there is exactly one input lifetime, it is assigned to all output lifetimes.
fn first_word(s: &str) -> &str {
// compiler infers: fn first_word<'a>(s: &'a str) -> &'a str
let i = s.find(' ').unwrap_or(s.len());
&s[..i]
}
Rule 3: If one of the inputs is &self or &mut self, its lifetime is assigned to all output lifetimes.
impl MyStruct {
fn get_name(&self) -> &str { // compiler infers output lifetime = 'self
&self.name
}
}
If none of these rules fully determine the output lifetimes, the compiler asks you to annotate explicitly.
Lifetimes in Structs
If a struct holds a reference, it must have a lifetime annotation to guarantee the struct doesn’t outlive the data it references:
struct Important<'a> {
text: &'a str,
}
impl<'a> Important<'a> {
fn announce(&self, topic: &str) -> &str {
println!("Attention regarding {}: {}", topic, self.text);
self.text
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence;
{
let i = novel.find('.').unwrap_or(novel.len());
first_sentence = &novel[..i];
}
let important = Important { text: first_sentence };
println!("{}", important.text);
}
The lifetime 'a ensures that Important cannot outlive the &str it holds.
Multiple Lifetime Parameters
When a function has references with independent lifetimes, you can use multiple parameters:
fn first_or_default<'a, 'b>(slice: &'a [&'b str], default: &'b str) -> &'b str {
slice.first().copied().unwrap_or(default)
}
Keep annotations minimal — only introduce new lifetime parameters when the relationships genuinely differ.
The 'static Lifetime
'static means the reference is valid for the entire program:
// String literals are baked into the binary — always 'static
let s: &'static str = "I have a static lifetime";
// Functions that require 'static (e.g., thread::spawn)
fn needs_static(s: &'static str) {
println!("{}", s);
}
Common sources of 'static data:
- String literals
Box::leak— leaks heap memory to produce a'staticreference (use sparingly)lazy_static!/once_cell— static initializers
Do not use 'static bounds just to avoid thinking about lifetimes — it is often too restrictive.
Lifetime Bounds on Generics
You can require that a generic type’s references live at least as long as a given lifetime:
use std::fmt::Display;
fn print_longest_with_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement: {}", ann);
if x.len() > y.len() { x } else { y }
}
Common Lifetime Errors and Fixes
Error: returning a reference to local data
// WRONG
fn bad() -> &str {
let s = String::from("hello");
&s // s is dropped here!
}
// FIX: return owned data
fn good() -> String {
String::from("hello")
}
Error: struct holds reference that outlives the struct
// WRONG — compiler asks for lifetime annotation
struct Excerpt {
text: &str, // ERROR: missing lifetime specifier
}
// FIX: add lifetime parameter
struct Excerpt<'a> {
text: &'a str,
}
Error: conflating lifetimes that should be independent
// This says: output lives as long as BOTH inputs — too restrictive
fn first_char<'a>(s: &'a str, _other: &'a str) -> &'a str {
&s[..1]
}
// Better: output only depends on s
fn first_char_fixed<'a>(s: &'a str, _other: &str) -> &'a str {
&s[..1]
}
Lifetimes are Not Runtime Costs
Lifetime annotations are purely compile-time constructs. They generate zero machine code. They are the compiler’s way of tracking how long references are valid, not runtime metadata.
Summary
| Concept | Meaning |
|---|---|
'a | A named lifetime parameter |
fn f<'a>(x: &'a T) -> &'a T | Output reference lives as long as input |
struct S<'a> { r: &'a T } | Struct cannot outlive the reference it holds |
'static | Reference valid for the entire program |
| Elision | Compiler infers lifetimes for common patterns |
| No annotation needed | When elision rules fully determine all lifetimes |