Lifetimes

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. A lifetime is the region of code during which a reference is valid. The borrow checker (see Borrowing & References) computes lifetimes for every reference; most of the time you never write them, because the compiler infers them inside function bodies and elides them in common signatures. You write lifetime annotations when a signature or a type must state how the lifetimes of several references relate. Annotations never change how long a value lives — they describe constraints that the compiler then checks.

Why lifetimes exist

Consider a function that returns the longer of two string slices. The returned reference points into one of the arguments, but the compiler cannot know which from the signature alone, so it cannot check callers:

fn longest(a: &str, b: &str) -> &str { // error[E0106]: missing lifetime specifier
    if a.len() >= b.len() { a } else { b }
}

fn main() {
    println!("{}", longest("Middlemarch", "Dune"));
}

Source: Error E0106

# error[E0106]: missing lifetime specifier
#  --> src/main.rs:1:33
#   |
# 1 | fn longest(a: &str, b: &str) -> &str {
#   |               ----     ----     ^ expected named lifetime parameter
#   |
#   = help: this function's return type contains a borrowed value, but the signature does not say whether it is
#           borrowed from `a` or `b`
# help: consider introducing a named lifetime parameter
#   |
# 1 | fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {

Annotation syntax

Lifetime parameters are named with an apostrophe and a short lowercase name ('a, 'de), declared in angle brackets like generic type parameters, and written after the & of a reference: &'a T, &'a mut T. The fix the compiler suggests says: "there is some lifetime 'a; both arguments live at least that long, and the result is valid for that long too".

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

fn main() {
    let a = String::from("Middlemarch");
    let result;
    {
        let b = String::from("Dune");
        result = longest(&a, &b);
        println!("{result}"); // fine: both a and b are alive
    }
    // println!("{result}"); // would not compile: `b` does not live long enough (E0597)
}

At each call site the compiler picks the concrete 'a as the intersection of the regions where both arguments are valid. The result therefore cannot be used after the shorter-lived argument is dropped:

Diagram of lifetimes as regions of code. A listing declares a = String::from("Middlemarch") and result in main, then opens an inner block that declares b = String::from("Dune"), assigns result = longest(&a, &b) and prints it. Bars beside the code show a alive until the end of main, b alive only inside the inner block, and result usable only where both overlap; a later use of result after the inner block is marked as rejected with E0597.

A signature only needs to relate the lifetimes that matter. If the result can only come from a, say so and b needs no relation:

fn first_word<'a>(text: &'a str, _separator: &str) -> &'a str {
    text.split(' ').next().unwrap_or(text)
}

fn main() {
    let title = String::from("Dune Messiah");
    let word;
    {
        let sep = String::from(" ");
        word = first_word(&title, &sep);
    } // `sep` dropped: fine, the result does not borrow from it
    println!("{word}");
}

Returning a reference to a local is impossible to annotate, because no caller-provided lifetime covers it (error E0515, "cannot return reference to local variable"). Return an owned value (String, Vec<T>) instead.

The elision rules

Writing lifetimes on every reference would be tedious, so the compiler applies three elision rules to function and method signatures. Lifetimes on inputs are input lifetimes; on the return type, output lifetimes.

  1. Each elided lifetime in the parameters becomes a distinct lifetime parameter.

  2. If there is exactly one input lifetime (elided or not), it is assigned to all elided output lifetimes.

  3. If there are several input lifetimes but one of them is &self or &mut self, the lifetime of self is assigned to all elided output lifetimes.

If the rules do not determine every output lifetime, an annotation is required (E0106).

Written After elision Rule

fn len(s: &str) -> usize

fn len<'a>(s: &'a str) -> usize

1

fn trim(s: &str) -> &str

fn trim<'a>(s: &'a str) -> &'a str

1, 2

fn get(&self, key: &str) -> &Book

fn get<'a, 'b>(&'a self, key: &'b str) -> &'a Book

1, 3

fn longest(a: &str, b: &str) -> &str

ambiguous: annotation required

 — 

Elision also applies in impl headers and to trait objects (Box<dyn Trait> means Box<dyn Trait + 'static>, and &'a dyn Trait means &'a (dyn Trait + 'a)). The anonymous lifetime '_ makes an elided lifetime visible without naming it, which helps readers: fn titles(&self) -> Iter<'_, String>. The elided_lifetimes_in_paths lint (part of rust_2018_idioms) asks for '_ in such paths.

Lifetimes in structs and impl blocks

A struct that holds a reference must declare a lifetime parameter, and an instance cannot outlive the data it borrows. This is useful for short-lived views, parsers and iterators that avoid copying:

/// A view of one line of a CSV export, borrowing from the input text.
struct Record<'a> {
    isbn: &'a str,
    title: &'a str,
}

impl<'a> Record<'a> {
    fn parse(line: &'a str) -> Option<Self> {
        let (isbn, title) = line.split_once(',')?;
        Some(Record { isbn: isbn.trim(), title: title.trim() })
    }

    // Elision rule 3: the result borrows from `self`, which is fine here...
    fn label(&self) -> String {
        format!("{} [{}]", self.title, self.isbn)
    }

    // ...but returning `&'a str` says the result borrows from the original input,
    // so it may outlive the Record itself.
    fn title(&self) -> &'a str {
        self.title
    }
}

fn main() {
    let export = String::from("9780441013593, Dune\n9780141439518, Emma");
    let titles: Vec<&str> = export.lines().filter_map(Record::parse).map(|r| r.title()).collect();
    println!("{titles:?}"); // the Records are gone, the &str still borrow `export`
    if let Some(r) = Record::parse("9780679732242, Beloved") {
        println!("{}", r.label());
    }
}

Structs with references are powerful but contagious: every type that contains a Record<'a> must also carry 'a. For long-lived data — application state, values sent to other threads, anything stored in a cache — prefer owned fields (String, Arc<str>). A struct cannot hold a reference to one of its own fields (a self-referential struct); see Pin & Unpin.

'static and T: 'static

'static is the lifetime of the whole program. A &'static T points to data that is never freed: string literals, static items, and memory leaked with Box::leak.

A bound T: 'static means something different: the type T contains no references with a shorter lifetime than 'static. Every owned type satisfies it — String, Vec<u8>, i32 — because they contain no references at all. That is why std::thread::spawn and tokio::spawn require F: 'static: the task may outlive the current function, so it must not borrow its locals, but it may own anything.

use std::fmt::Debug;

fn keep_forever<T: Debug + Send + 'static>(value: T) -> std::thread::JoinHandle<()> {
    std::thread::spawn(move || println!("{value:?}"))
}

fn main() {
    let owned = String::from("Dune");
    keep_forever(owned).join().unwrap();       // String: 'static -- it owns its data
    keep_forever("literal").join().unwrap();   // &'static str: 'static
    let local = String::from("Emma");
    // keep_forever(&local);                    // would not compile: `local` does not live long enough (E0597)
    keep_forever(local.clone()).join().unwrap();
}

When a compiler message says "the parameter type T may not live long enough" and suggests T: 'static, the real question is usually whether you should pass owned data, use scoped threads (thread::scope, which allows borrowing), or share with Arc.

Lifetime bounds and implied bounds

Lifetimes can be constrained like type parameters:

  • 'a: 'b, read as 'a outlives 'b — 'a lasts at least as long as 'b;

  • T: 'a — every reference inside T lives at least as long as 'a.

Many bounds are implied and do not need to be written: a parameter of type &'a T already implies T: 'a (a reference cannot outlive what it points to), and a struct field &'a T implies the same for the struct.

struct Shelf<'a, T> {
    items: &'a [T], // implies T: 'a
}

// `'long: 'short` lets us return the longer-lived reference as the shorter one.
fn pick<'short, 'long: 'short>(fallback: &'short str, preferred: Option<&'long str>) -> &'short str {
    preferred.unwrap_or(fallback)
}

fn main() {
    let items = [1, 2, 3];
    let shelf = Shelf { items: &items };
    let fallback = String::from("untitled");
    println!("{} {}", shelf.items.len(), pick(&fallback, Some("Dune")));
}

Subtyping and variance

Lifetimes introduce subtyping into Rust: if 'long: 'short, then &'long T is a subtype of &'short T — a reference that is valid for longer can be used where a shorter one is expected. That is why a &'static str can be passed to any function taking &'a str.

Variance describes how subtyping of a type’s parameter carries over to the type itself:

Type Variance in 'a / T Consequence

&'a T

covariant / covariant

a longer-lived reference or a more specific T can be used

&'a mut T

covariant / invariant

T must match exactly; you cannot write a short-lived value through a &mut expecting a long-lived one

Box<T>, Vec<T>, Option<T>

covariant

behave like the value they hold

Cell<T>, RefCell<T>, UnsafeCell<T>

invariant

interior mutability allows writes through &

fn(T) -> U

contravariant in T, covariant in U

a function accepting more general arguments can stand in

PhantomData<T>

covariant

used to declare the variance of a type that stores T indirectly

Invariance of &mut T is what prevents this unsound program — if &mut &'static str could become &mut &'a str, you could store a short-lived string where a 'static one is expected:

fn overwrite<'a>(target: &mut &'a str, value: &'a str) {
    *target = value;
}

fn main() {
    let mut forever: &'static str = "Dune";
    {
        let temporary = String::from("Emma");
        overwrite(&mut forever, &temporary); // error[E0597]: `temporary` does not live long enough
    }
    println!("{forever}");
}

Variance is computed automatically from a type’s fields. It mainly matters when you write generic containers with raw pointers or PhantomData (see Generics & Const Generics) and when an error mentions that a type "is invariant over the parameter". Azam (Advanced Rust, chapter 6) discusses variance at length; the Reference’s "Subtyping and Variance" chapter is the normative source.

Higher-ranked trait bounds: for<'a>

Sometimes a bound must hold for every lifetime, not one chosen by the caller — typically for closures that receive a reference created inside the function. The syntax is for<'a>, a higher-ranked trait bound (HRTB):

fn apply_to_titles<F>(titles: &[String], f: F) -> Vec<usize>
where
    F: for<'a> Fn(&'a str) -> usize, // f must accept a &str of *any* lifetime
{
    titles.iter().map(|t| f(t.as_str())).collect()
}

fn main() {
    let titles = vec![String::from("Dune"), String::from("Middlemarch")];
    println!("{:?}", apply_to_titles(&titles, |s| s.len())); // [4, 11]
}

You rarely write for<'a> explicitly: the Fn(&str) -> usize sugar already means for<'a> Fn(&'a str) -> usize. It appears in bounds on traits with lifetime parameters, such as for<'de> Deserialize<'de> (which serde provides as DeserializeOwned).

impl Trait capture rules and use<..> (2024 edition)

A function returning impl Trait (return-position impl Trait, RPIT) returns a hidden type. The hidden type may borrow from the arguments, and the question is which generic parameters and lifetimes it is allowed to capture.

  • Until the 2021 edition, RPIT in free functions captured all type parameters but no lifetimes unless they appeared in the bounds, which forced the impl Trait + '_ or "Captures trick" workarounds.

  • In the 2024 edition, RPIT captures all in-scope generic parameters, including lifetimes, by default. That is usually what you want.

  • When you want to capture less, list exactly what is captured with a precise capturing bound use<..> (stable since 1.82).

// 2024: the returned iterator implicitly captures the lifetime of `titles`.
fn long_titles(titles: &[String], min: usize) -> impl Iterator<Item = &String> {
    titles.iter().filter(move |t| t.len() >= min)
}

// The result does not borrow from `prefix`: say so with use<..>, so callers can drop `prefix` early.
fn numbered<'a>(titles: &'a [String], prefix: &str) -> impl Iterator<Item = String> + use<'a> {
    let prefix = prefix.to_owned();
    titles.iter().enumerate().map(move |(i, t)| format!("{prefix}{i}: {t}"))
}

fn main() {
    let titles = vec![String::from("Dune"), String::from("Middlemarch")];
    println!("{:?}", long_titles(&titles, 5).collect::<Vec<_>>());
    let lines = {
        let prefix = String::from("#");
        numbered(&titles, &prefix) // fine: the iterator does not capture `prefix`'s lifetime
    };
    for line in lines {
        println!("{line}");
    }
}

cargo fix --edition adds use<..> bounds where the new default would capture more than the old code did. In traits, impl Trait in return position (RPITIT, 1.75) always captured all lifetimes.

Reading lifetime errors

Error How to read it

E0106 missing lifetime specifier

a returned or stored reference could come from more than one place: add a lifetime that names the source, or return an owned value

E0597 borrowed value does not live long enough

the referent is dropped while a reference is still used: move the owner to an outer scope or own the data

E0515 cannot return reference to local variable

return the owned value instead

E0621 explicit lifetime required in the type of …​

the signature promises 'a for the result, but it comes from a parameter without 'a: add 'a to that parameter

"lifetime may not live long enough"

a value with one lifetime is returned or stored where a longer one is required; check which parameter the result actually borrows from, and whether a 'b: 'a bound is missing

"the parameter type T may not live long enough"

a generic value is stored somewhere requiring 'static (a thread, Box<dyn Any>): add T: 'static, or avoid borrowing

Older compilers reported many of these as E0495 ("cannot infer an appropriate lifetime"); since NLL became the only borrow checker (Rust 1.63), that code is no longer emitted and the messages name the conflicting lifetimes directly.

Where the books are outdated

Azam (Advanced Rust, chapter 6) covers concrete versus generic lifetimes, 'static, elision, bounds, implied bounds and variance; it predates the 2024 edition’s RPIT capture rules and does not cover use<..> precise capturing (1.82). Code following the older capture rules may carry impl Trait + '_ bounds that are now redundant.