Skip to content

Lifetimes Deep Dive ​

Updated Mar 2026

Advanced 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:

rust
let x = 0;
let y = &x;
let z = &y;

With explicit lifetime regions:

rust
'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 ​

rust
// 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 ​

rust
// 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 ​

rust
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 ​

rust
// 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 ​

rust
fn as_str<'a>(data: &'a u32) -> &'a str {
    let s = format!("{}", data);
    &s  // ERROR: s is dropped at end of function
}

Desugared:

rust
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:

rust
fn to_string(data: &u32) -> String {
    format!("{}", data)
}

Aliased Mutable References -- The Deep Reason ​

rust
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:

rust
'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:

rust
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 borrow

Lifetime Pauses ​

NLL allows lifetimes to have gaps:

rust
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 borrow

Drop and Lifetimes ​

If a type has a Drop implementation, the destructor counts as a final "use" at the end of scope:

rust
#[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 now

The 'static Lifetime ​

'static means the reference is valid for the entire program duration:

rust
// 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.

rust
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 -- ERROR

Variance 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.

VarianceRuleExample
Covariant'long can be used where 'short is expected&'a T is covariant in 'a
Contravariant'short can be used where 'long is expectedfn(&'a T) is contravariant in 'a
InvariantLifetimes must match exactly&'a mut T is invariant in T

Why &mut T is Invariant ​

rust
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:

rust
// 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 ​

rust
// 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 ​

rust
// 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 ​

rust
// 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 ​

rust
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 ​

rust
// 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_cell

Quick Reference ​

ConceptSyntaxMeaning
Lifetime parameter'aNamed lifetime region
Static lifetime'staticValid for entire program
Lifetime boundT: 'aT outlives lifetime 'a
HRTBfor<'a> Fn(&'a T)Works for any lifetime
Wildcard lifetime'_Inferred by compiler
Elision(no annotation)Compiler infers per rules

Built with VitePress | Rust SOP Documentation