Phase 2Ownership & borrowing

#8 Lifetimes

'a, lifetime annotations

Why lifetimes?

Every reference in Rust has a lifetime — the scope for which that reference is valid. Most of the time, lifetimes are inferred automatically, just like types. But sometimes the compiler needs your help to understand how long references should live.

The problem
// This does NOT compile — which reference should we return?
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
// Error: missing lifetime specifier

The compiler cannot determine whether the returned reference comes from x or y, so it does not know how long the return value will be valid. Lifetime annotations solve this.

Lifetime annotations

Lifetime annotations describe the relationships between the lifetimes of references. They start with an apostrophe ('a):

Lifetime annotations
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("hello");
    let result;
    {
        let s2 = String::from("world!");
        result = longest(&s1, &s2);
        println!("Longest: {result}");
    }
}

The annotation 'a tells the compiler: the returned reference will be valid for the shorter of the two input lifetimes. It does not change how long values live — it helps the compiler verify that references are used safely.

Lifetime elision

Rust has lifetime elision rules that let you omit annotations in common cases. The compiler applies these rules automatically:

Elision rules
// Rule 1: Each reference parameter gets its own lifetime
// Rule 2: If there's exactly one input lifetime, it's assigned to all outputs
// Rule 3: If one parameter is &self or &mut self, its lifetime is used

// You write:
fn first_word(s: &str) -> &str { /* ... */ }

// The compiler sees:
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ }

// You write:
fn len(s: &str) -> usize { s.len() }

// No output reference, so no lifetime needed on return type

Thanks to elision, you rarely need to write lifetime annotations. The compiler will tell you when it needs them.

Lifetimes in structs

When a struct holds a reference, it needs a lifetime annotation to ensure the referenced data lives at least as long as the struct:

Struct with lifetime
struct Excerpt<'a> {
    text: &'a str,
}

impl<'a> Excerpt<'a> {
    fn level(&self) -> i32 {
        3
    }

    fn announce_and_return(&self, announcement: &str) -> &str {
        println!("Announcement: {announcement}");
        self.text
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let excerpt = Excerpt { text: first_sentence };
    println!("Excerpt: {}", excerpt.text);
}

The 'a annotation means: the Excerpt struct cannot outlive the string it references. The compiler enforces this at every call site.

The 'static lifetime

The special lifetime 'static means the reference lives for the entire program. String literals have this lifetime:

'static lifetime
let s: &'static str = "I live forever!";

// String literals are stored in the binary,
// so they're always valid for the program's duration.

Your turn

Try running the lifetimes demo with cargo run and cargo check in the terminal below:

terminal — cargo
user@stemlegacy:~/lifetimes-demo$