Lifetimes Deep Dive ​
Updated Mar 2026Advanced lifetime patterns beyond the basics. Covers lifetime elision rules, variance, subtyping, higher-ranked trait bounds, Non-Lexical Lifetimes (NLL), and common pitfalls.
What Lifetimes Really Are ​
Lifetimes are named regions of code that a reference must be valid for. They correspond to paths of execution in the program, not simple scopes.
Desugaring Lifetimes ​
Simple code:
let x = 0;
let y = &x;
let z = &y;With explicit lifetime regions:
'a: {
let x: i32 = 0;
'b: {
let y: &'b i32 = &'b x;
'c: {
let z: &'c &'b i32 = &'c y;
}
}
}Each variable lives in a lifetime region. References must not outlive the data they point to.
Lifetime Elision Rules ​
The compiler applies three rules to infer lifetimes on function signatures. If any output lifetimes remain ambiguous after all three rules, you must annotate manually.
Rule Examples ​
// Rule 1 only: each parameter gets its own lifetime
fn first_word(s: &str) -> &str
// Becomes: fn first_word<'a>(s: &'a str) -> &'a str (Rule 2 assigns output)
// Rule 1 + fails without annotation:
fn longest(x: &str, y: &str) -> &str // ERROR: ambiguous output
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str // FIXED: manual annotation
// Rule 3: method with &self
impl ImportantExcerpt<'_> {
fn announce(&self, announcement: &str) -> &str {
// Output gets self's lifetime (Rule 3)
self.part
}
}Lifetime Annotations in Practice ​
In Functions ​
// The returned reference is valid for the shorter of 'a and 'b
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}The single 'a means: "the output lives at least as long as the shorter-lived input." The caller's usage determines the concrete lifetime.
In Structs ​
struct ImportantExcerpt<'a> {
part: &'a str, // struct cannot outlive the data it borrows
}
// This won't compile:
// let excerpt;
// {
// let novel = String::from("Call me Ishmael...");
// excerpt = ImportantExcerpt { part: &novel };
// } // novel dropped, but excerpt still holds a reference
// println!("{}", excerpt.part);Multiple Lifetimes ​
// x and y have independent lifetimes
// Output borrows from x only
fn first_of_two<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str {
x
}Why Returning Local References Fails ​
fn as_str<'a>(data: &'a u32) -> &'a str {
let s = format!("{}", data);
&s // ERROR: s is dropped at end of function
}Desugared:
fn as_str<'a>(data: &'a u32) -> &'a str {
'b: {
let s = format!("{}", data);
return &'a s; // s lives in 'b, but we promised 'a
} // s dropped here
}The contract says the returned reference must outlive 'a, but s is created in the inner scope 'b. Since 'b ends before 'a, this is unsound. The fix is to return an owned value:
fn to_string(data: &u32) -> String {
format!("{}", data)
}Aliased Mutable References -- The Deep Reason ​
let mut data = vec![1, 2, 3];
let x = &data[0]; // immutable borrow of data
data.push(4); // ERROR: mutable borrow while immutable borrow exists
println!("{}", x);The deep reason: push may reallocate the vector's buffer. If it does, x becomes a dangling pointer. Rust's borrow checker prevents this at compile time.
Desugared with lifetime regions:
'a: {
let mut data: Vec<i32> = vec![1, 2, 3];
'b: {
let x: &'b i32 = Index::index::<'b>(&'b data, 0);
'c: {
Vec::push(&'c mut data, 4); // needs &mut data, but &data still borrowed for 'b
}
println!("{}", x); // x used here, so 'b extends past 'c
}
}Non-Lexical Lifetimes (NLL) ​
Before Rust 2018, lifetimes extended to the end of the enclosing scope. NLL shrinks lifetimes to the last use of a reference:
let mut data = vec![1, 2, 3];
let x = &data[0];
println!("{}", x); // last use of x -- borrow ends here
data.push(4); // OK with NLL: no active immutable borrowLifetime Pauses ​
NLL allows lifetimes to have gaps:
let mut data = vec![1, 2, 3];
let mut x = &data[0];
println!("{}", x); // last use of first borrow
data.push(4); // OK: first borrow ended
x = &data[3]; // new borrow starts
println!("{}", x); // last use of second borrowDrop and Lifetimes ​
If a type has a Drop implementation, the destructor counts as a final "use" at the end of scope:
#[derive(Debug)]
struct X<'a>(&'a i32);
impl Drop for X<'_> {
fn drop(&mut self) {}
}
let mut data = vec![1, 2, 3];
let x = X(&data[0]);
println!("{:?}", x);
// data.push(4); // ERROR: x's destructor runs at end of scope
drop(x); // fix: explicitly drop x first
data.push(4); // OK nowThe 'static Lifetime ​
'static means the reference is valid for the entire program duration:
// String literals are always 'static
let s: &'static str = "I have a static lifetime";
// Owned data can satisfy 'static bounds (no borrowed data)
fn print_it(input: impl std::fmt::Display + 'static) {
println!("{}", input);
}
print_it(String::from("owned")); // OK: String has no borrowed data
// print_it(&local); // ERROR: reference is not 'static'static as a Trait Bound ​
T: 'static does NOT mean "must be a static reference." It means "T contains no non-static references." Owned types like String, Vec<u32>, and i32 all satisfy 'static.
fn spawn_thread<T: Send + 'static>(val: T) {
std::thread::spawn(move || {
println!("got: {:?}", val);
});
}
spawn_thread(42); // i32: 'static -- OK
spawn_thread(String::from("hello")); // String: 'static -- OK
// spawn_thread(&local_var); // &'a str: NOT 'static -- ERRORVariance and Subtyping ​
Variance describes how lifetime parameters relate when types are nested. This is critical for understanding why some seemingly correct code fails to compile.
| Variance | Rule | Example |
|---|---|---|
| Covariant | 'long can be used where 'short is expected | &'a T is covariant in 'a |
| Contravariant | 'short can be used where 'long is expected | fn(&'a T) is contravariant in 'a |
| Invariant | Lifetimes must match exactly | &'a mut T is invariant in T |
Why &mut T is Invariant ​
fn evil(input: &mut &'static str) {
// If covariant, we could shrink 'static to 'a
// and then assign a short-lived string
// That would create a dangling reference!
}Mutable references must be invariant in their referent type to prevent such unsoundness.
Higher-Ranked Trait Bounds (HRTB) ​
When you need a function that works with references of any lifetime:
// Without HRTB -- doesn't work generically:
// fn apply<'a>(f: impl Fn(&'a str)) { f("hello"); }
// With HRTB -- works for any lifetime:
fn apply(f: impl for<'a> Fn(&'a str)) {
f("hello");
}
// In practice, Rust infers HRTB for closures:
fn apply2(f: impl Fn(&str)) {
// This is sugar for: impl for<'a> Fn(&'a str)
f("hello");
}Where HRTB Appears ​
// Trait objects with HRTB
trait Parser {
fn parse<'a>(&self, input: &'a str) -> &'a str;
}
// The trait object needs HRTB to express "any lifetime"
fn use_parser(parser: &dyn for<'a> Fn(&'a str) -> &'a str) {
let result = parser("hello world");
println!("{}", result);
}Lifetime Bounds on Generic Types ​
// T must outlive 'a
struct Ref<'a, T: 'a> {
value: &'a T,
}
// T: 'static means T contains no non-static references
struct Owned<T: 'static> {
value: T,
}
// Combining lifetime bounds
fn print_ref<'a, T>(t: &'a T)
where
T: std::fmt::Debug + 'a,
{
println!("{:?}", t);
}Common Lifetime Pitfalls ​
Pitfall 1: Lifetime Too Short for Return Value ​
// WRONG: returning reference to local data
fn bad() -> &str {
let s = String::from("hello");
&s // s is dropped, reference dangles
}
// FIX: return owned data
fn good() -> String {
String::from("hello")
}Pitfall 2: Borrowing Across Closures ​
let mut data = vec![1, 2, 3];
// WRONG: closure borrows data mutably, but data is already borrowed
// let closure = || data.push(4);
// println!("{:?}", data);
// closure();
// FIX: sequence the operations
data.push(4);
println!("{:?}", data);Pitfall 3: Self-Referential Structs ​
// IMPOSSIBLE in safe Rust:
struct SelfRef {
data: String,
// slice: &str, // can't reference data -- struct might move!
}
// Solutions:
// 1. Use indices instead of references
// 2. Use Pin<Box<T>> with unsafe
// 3. Use crates like ouroboros or self_cellQuick Reference ​
| Concept | Syntax | Meaning |
|---|---|---|
| Lifetime parameter | 'a | Named lifetime region |
| Static lifetime | 'static | Valid for entire program |
| Lifetime bound | T: 'a | T outlives lifetime 'a |
| HRTB | for<'a> Fn(&'a T) | Works for any lifetime |
| Wildcard lifetime | '_ | Inferred by compiler |
| Elision | (no annotation) | Compiler infers per rules |