Rust Lifetimes

Why Lifetimes Exist

Prevent dangling references at compile time
// 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

Tell the compiler how reference lifetimes relate
// 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

When the compiler infers lifetimes automatically
// 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

Structs holding references need lifetime annotations
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

'static — lives for the entire program
// 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

Returning one of multiple borrows
// 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.