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:

  1. Each reference parameter gets its own lifetime
  2. If there’s exactly one input lifetime, it’s assigned to all output references
  3. If one parameter is &self or &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 (String vs &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: 'static doesn’t mean the value lives forever. It means the type doesn’t contain any non-static references. String is 'static (it owns its data). &'a str is only 'static when 'a = 'static.

When You Hit Lifetime Walls

SymptomSolution
”does not live long enough”Move data into the struct (own it) instead of borrowing
Too many lifetime parametersUse owned types (String, Vec) instead of references
Self-referential structUse indices or Pin<Box<T>>, or redesign the data model
”cannot return reference to local variable”Return an owned value instead
Complex lifetime relationshipsConsider 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]
    }
}

Next: Ecosystem | Ownership