Skip to content
Lifetimes In Depth
step 1/7

Reading — step 1 of 7

Learn

~3 min readLifetimes, Trait Objects, and Async

Lifetimes are how Rust's borrow checker tracks how long references are valid. The Rust Book Ch 10 introduces them; Programming Rust Ch 5 goes deep. Lifetimes are notation for what the borrow checker is already doing — making the implicit constraints explicit.

Lifetime annotations on references

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

'a is a lifetime parameter. The signature says: "both inputs and the output share lifetime 'a — the result is valid as long as BOTH inputs are."

The borrow checker then enforces: callers can only use the return value while both arguments are still alive.

Why we need them

Without annotations, Rust can't tell which input the output borrows from:

fn longest(x: &str, y: &str) -> &str {  // ERROR: missing lifetime
    if x.len() > y.len() { x } else { y }
}

The compiler can't know if the returned reference points at x, at y, or at something else. Lifetime annotations express the relationship.

Lifetime elision rules

In many cases, Rust infers lifetimes for you. The three rules:

  1. Each parameter gets its own lifetimefn foo(x: &str, y: &str) becomes fn foo<'a, 'b>(x: &'a str, y: &'b str)
  2. If there's exactly one input lifetime, it's assigned to all output lifetimes
  3. If there's &self or &mut self, its lifetime is assigned to all outputs

So fn first_word(s: &str) -> &str works without annotations — rule 2 elides them. The annotations are only required when the rules can't disambiguate.

Lifetimes in structs

If a struct holds a reference, you must annotate:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn announce(&self, msg: &str) -> &str {
        println!("{}", msg);
        self.part
    }
}

This means: the struct cannot outlive the data its part field references.

The 'static lifetime

'static lives for the entire program. String literals have &'static str — they're embedded in the binary:

let s: &'static str = "hello";    // valid for the whole program

Be cautious about requiring 'static in APIs — it forces ownership or eternal data. Often 'a for some generic lifetime is what you actually need.

Lifetime bounds on generics

fn longest_with_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: std::fmt::Display,
{
    println!("announcement: {}", ann);
    if x.len() > y.len() { x } else { y }
}

T can be any type that implements Display; the lifetime 'a constrains the references.

Higher-ranked trait bounds (HRTB)

For functions that should work with any lifetime:

fn apply<F>(f: F)
where
    F: for<'a> Fn(&'a str) -> &'a str,
{
    let local = String::from("hello");
    let result = f(&local);
    println!("{}", result);
}

for<'a> says "f must work for any choice of 'a". This is what lets Fn traits accept references to local data.

Common mistakes

  • Returning a reference to a local variable — fundamental error; the local goes out of scope when the function returns.
  • Trying to satisfy 'static when 'a would do — over-constrains the API.
  • Naming lifetimes 'a, 'b, 'c without meaning — rename to 'src, 'dest, etc. when there's a semantic distinction.
  • Lifetimes in trait bounds without HRTBF: Fn(&str) is implicitly for<'a> Fn(&'a str) for closures, but it matters when reasoning about the bound.
  • Adding lifetime parameters everywhere — in many cases elision handles it. Only annotate when the compiler asks.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…