Skip to content
AgileLogics
5 min read

Lifetimes

Prerequisites: Common standard traits

Every reference in Rust has a lifetime: the region of code during which it is valid. The borrow checker uses lifetimes to guarantee that no reference outlives the data it points to, so dangling pointers are impossible.

Most of the time lifetimes are inferred and you never write them. You need to annotate them when the compiler can't work out how the lifetimes of a function's inputs relate to its output, or when a struct stores a reference.

Lifetime annotations don't change how long anything lives. They describe relationships ("the returned reference is valid as long as both arguments are") so the compiler can check every caller against them.

Syntax

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

struct Excerpt<'a> {                                    // a struct holding a reference
    part: &'a str,
}

impl<'a> Excerpt<'a> {
    fn part(&self) -> &str {                            // elided: tied to &self
        self.part
    }
}

Lifetime names start with an apostrophe and are usually short: 'a, 'b. <'a> declares the parameter, just like <T> declares a type parameter.

Why longest needs an annotation

longest returns either x or y, and the compiler can't know which one at compile time. Without annotations it has no idea how long the result is allowed to be used. 'a says: the result is valid for the shorter of the two input lifetimes. Callers are then checked against that promise.

Example

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

// The result depends only on `text`, so only `text` shares the lifetime.
fn first_sentence<'a>(text: &'a str, _separator_hint: &str) -> &'a str {
    text.split('.').next().unwrap_or(text)
}

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

impl<'a> Excerpt<'a> {
    fn new(part: &'a str) -> Self {
        Excerpt { part }
    }

    fn word_count(&self) -> usize {
        self.part.split_whitespace().count()
    }
}

fn main() {
    let a = String::from("a fairly long string");
    let result;
    {
        let b = String::from("short");
        result = longest(&a, &b);
        println!("longest: {result}");   // fine: both `a` and `b` are still alive here
    }

    let novel = String::from("Call me Ishmael. Some years ago...");
    let excerpt = Excerpt::new(first_sentence(&novel, "."));
    println!("excerpt: '{}' ({} words)", excerpt.part, excerpt.word_count());

    let greeting: &'static str = "string literals live for the whole program";
    println!("{greeting}");
}

Expected output

longest: a fairly long string
excerpt: 'Call me Ishmael' (3 words)
string literals live for the whole program

Excerpt<'a> means "an Excerpt cannot outlive the string it borrows from". If novel were dropped while excerpt was still in use, the program would not compile.

Lifetime elision

To save typing, the compiler applies three elision rules to function signatures. If they fully determine the output lifetime, you don't write any:

  1. Each reference parameter gets its own lifetime.
  2. If there is exactly one input lifetime, it is used for every output reference: fn first_word(s: &str) -> &str needs no annotation.
  3. If one of the parameters is &self or &mut self, its lifetime is used for the outputs. That is why methods rarely need annotations.

longest has two reference inputs and no self, so none of the rules decide the output — hence the annotation.

'static is a special lifetime meaning "valid for the entire program". All string literals are &'static str. Don't reach for 'static to silence a lifetime error; it usually means an owned value (String) is what you need.

Common mistakes

  • Returning a reference from a function with two reference parameters and no annotation: error[E0106]: missing lifetime specifier. The compiler suggests adding 'a.

  • Using the result after one of the inputs is gone:

    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
        if x.len() >= y.len() { x } else { y }
    }
    
    fn main() {
        let a = String::from("long string is long");
        let result;
        {
            let b = String::from("xyz");
            result = longest(a.as_str(), b.as_str());
        }
        println!("{result}");
    }
    

    This fails with error[E0597]: `b` does not live long enough: result might refer to b, which is dropped at the closing brace.

  • Returning a reference to a local variable. No annotation can fix that; the value is dropped when the function returns. Return the owned value (String, Vec<T>) instead.

  • Putting references in structs by default. Structs that own their data (String rather than &str) are much easier to use; add lifetimes to a struct only when borrowing is a deliberate choice.

Practice

  1. Write fn longer_slice<'a>(a: &'a [i32], b: &'a [i32]) -> &'a [i32] that returns the slice with more elements (a on a tie).
  2. Write a struct Highlight<'a> holding text: &'a str, with a method first_word(&self) -> &str that returns the text up to the first space.

Print the longer of [1, 2, 3] and [4, 5], and the first word of a Highlight built from "borrow checker rules".

Show solution
fn longer_slice<'a>(a: &'a [i32], b: &'a [i32]) -> &'a [i32] {
    if a.len() >= b.len() { a } else { b }
}

struct Highlight<'a> {
    text: &'a str,
}

impl<'a> Highlight<'a> {
    // Elision rule 3: the returned &str is tied to &self.
    fn first_word(&self) -> &str {
        self.text.split(' ').next().unwrap_or(self.text)
    }
}

fn main() {
    let x = [1, 2, 3];
    let y = [4, 5];
    println!("{:?}", longer_slice(&x, &y));

    let sentence = String::from("borrow checker rules");
    let h = Highlight { text: &sentence };
    println!("{}", h.first_word());
}
[1, 2, 3]
borrow