In the previous tutorial, we learned generics. Now we learn lifetimes — the last piece of Rust’s ownership system.

Lifetimes answer one question: “How long does this reference live?” The Rust compiler uses lifetimes to make sure every reference is always valid. No dangling pointers. No use-after-free bugs. No segfaults.

If you have struggled with lifetime errors, this tutorial will help. Most of the time, lifetimes are invisible — the compiler figures them out for you. But sometimes you need to tell the compiler what you mean.

The Problem Lifetimes Solve

Look at this function:

fn longer(s1: &str, s2: &str) -> &str {
    if s1.len() >= s2.len() {
        s1
    } else {
        s2
    }
}

This does not compile. The error says:

error[E0106]: missing lifetime specifier
 --> src/main.rs:1:40
  |
1 | fn longer(s1: &str, s2: &str) -> &str {
  |               ----      ----     ^ expected named lifetime parameter

Why? The function returns a reference, but the compiler does not know which input it comes from. Does it come from s1? Or s2? The compiler needs to know so it can check that the returned reference is still valid.

Lifetime Annotations

Lifetime annotations tell the compiler how the lifetimes of references relate to each other. They use an apostrophe and a short name:

fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() >= s2.len() {
        s1
    } else {
        s2
    }
}

The 'a means: “The returned reference lives at least as long as the shorter of the two input lifetimes.” This is not changing how long the references live. It is telling the compiler the relationship.

fn main() {
    let string1 = String::from("long string");
    let result;
    {
        let string2 = String::from("short");
        result = longer(&string1, &string2);
        println!("Longer: {}", result);  // Works — both references are valid here
    }
    // println!("{}", result);  // Would NOT compile if uncommented
    // string2 is dropped, so result might point to invalid memory
}

The compiler knows that result might reference string2, so it prevents you from using result after string2 is dropped.

Lifetime Annotations Do Not Change Lifetimes

This is the most important thing to understand. Annotations are like type annotations — they describe what is there, they do not create something new.

fn first_word<'a>(text: &'a str) -> &'a str {
    match text.find(' ') {
        Some(pos) => &text[..pos],
        None => text,
    }
}

The 'a says: “The returned reference lives as long as the input reference.” It does not extend or shorten any lifetime. It just tells the compiler the relationship so it can check your code.

Lifetime Elision Rules

Most of the time, you do not need to write lifetime annotations. The Rust compiler has three elision rules that figure out lifetimes automatically:

Rule 1: Each reference parameter gets its own lifetime.

fn foo(x: &str)              // becomes fn foo<'a>(x: &'a str)
fn foo(x: &str, y: &str)     // becomes fn foo<'a, 'b>(x: &'a str, y: &'b str)

Rule 2: If there is exactly one input lifetime, it is assigned to all output lifetimes.

fn first_word(text: &str) -> &str
// becomes: fn first_word<'a>(text: &'a str) -> &'a str

Rule 3: If one of the parameters is &self or &mut self, the lifetime of self is assigned to all output lifetimes.

impl MyStruct {
    fn get_name(&self) -> &str  // self's lifetime goes to output
}

If the compiler cannot figure out lifetimes after applying all three rules, it asks you to add annotations. That is when you get the “missing lifetime specifier” error.

Examples That Do Not Need Annotations

// Rule 2: one input → output gets same lifetime
fn trim_spaces(text: &str) -> &str {
    text.trim()
}

// Rule 3: &self → output gets self's lifetime
struct TextProcessor {
    prefix: String,
}

impl TextProcessor {
    fn get_prefix(&self) -> &str {
        &self.prefix
    }

    fn process(&self, text: &str) -> String {
        format!("{}: {}", self.prefix, text)
    }
}

These all work without explicit lifetimes because the compiler applies the elision rules.

Lifetimes in Structs

When a struct holds a reference, it must have a lifetime annotation:

#[derive(Debug)]
struct Excerpt<'a> {
    text: &'a str,
}

The 'a means: “An Excerpt cannot outlive the reference it holds.” The compiler enforces this:

fn main() {
    let excerpt;
    {
        let text = String::from("Rust is fast. Rust is safe.");
        excerpt = Excerpt { text: &text };
        println!("{:?}", excerpt);  // Works here
    }
    // println!("{:?}", excerpt);  // ERROR: text is dropped
}

Methods on Structs with Lifetimes

Methods follow the same rules. Rule 3 usually handles things:

impl<'a> Excerpt<'a> {
    // Returns &str — Rule 3 gives it self's lifetime
    fn first_sentence(&self) -> &str {
        match self.text.find('.') {
            Some(pos) => &self.text[..=pos],
            None => self.text,
        }
    }

    // Takes another reference — returns owned String, no lifetime needed
    fn announce(&self, announcement: &str) -> String {
        format!("{}: {}", announcement, self.text)
    }
}

Multiple Lifetime Parameters

Sometimes a struct holds references with different lifetimes:

#[derive(Debug)]
struct Comparison<'a, 'b> {
    left: &'a str,
    right: &'b str,
}

impl<'a, 'b> Comparison<'a, 'b> {
    fn new(left: &'a str, right: &'b str) -> Comparison<'a, 'b> {
        Comparison { left, right }
    }

    fn both(&self) -> String {
        format!("{} vs {}", self.left, self.right)
    }
}

This is rare. Most structs need only one lifetime parameter. Use multiple lifetimes when the references truly have independent lifetimes.

The Static Lifetime

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

fn get_greeting() -> &'static str {
    "Hello, world!"  // String literals are always 'static
}

fn get_language() -> &'static str {
    "Rust"
}

String literals are stored in the program’s binary. They exist from start to finish. That is why they are 'static.

Warning: Do not slap 'static on everything to make the compiler happy. If the compiler asks for a lifetime, adding 'static usually means you are hiding a design problem. Use 'static only when the data truly lives forever (like string literals or leaked memory).

Common Lifetime Patterns

Pattern 1: Return the Longer String

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

Pattern 2: Find Something in a String

fn find_after<'a>(text: &'a str, marker: &str) -> &'a str {
    match text.find(marker) {
        Some(pos) => &text[pos + marker.len()..],
        None => "",
    }
}

Notice that marker does not need 'a. The output comes from text, not from marker. Rule 1 gives them separate lifetimes, and since the output clearly comes from text, you only annotate that relationship.

Wait — actually this compiles without annotations because of the elision rules? No, it does not when there are two reference inputs and one reference output (Rule 2 only works with one input). You need the annotation on text and the return value.

Let me clarify. With two input references and one output reference, the compiler cannot apply Rule 2 (which requires exactly one input lifetime). So you need to annotate which input the output comes from:

fn find_after<'a>(text: &'a str, marker: &str) -> &'a str {
    match text.find(marker) {
        Some(pos) => &text[pos + marker.len()..],
        None => "",
    }
}

Here text has lifetime 'a and the output has lifetime 'a. The marker parameter gets its own anonymous lifetime — the compiler does not care how long it lives because the output does not depend on it.

Pattern 3: Split and Return Part

fn before_colon<'a>(text: &'a str) -> &'a str {
    match text.find(':') {
        Some(pos) => &text[..pos],
        None => text,
    }
}

Actually, this has only one input reference, so Rule 2 handles it. You do not need the annotation. But writing it explicitly is fine — it makes the relationship clear.

Lifetimes with Generics

You can combine lifetimes with generic types:

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 }
}

The lifetime 'a and the generic T are both declared in the angle brackets. The lifetime relates to the string references. The generic T is for the announcement, which is unrelated to lifetimes.

Common Mistakes

Mistake: Returning a Reference to a Local Variable

fn create_greeting<'a>() -> &'a str {
    let s = String::from("hello");
    &s  // ERROR: s is dropped at end of function
}

No lifetime annotation can fix this. The data does not live long enough. Fix: Return an owned value:

fn create_greeting() -> String {
    String::from("hello")
}

Mistake: Thinking Annotations Extend Lifetimes

fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    // 'a does NOT make the strings live longer
    // It just tells the compiler the relationship
    if s1.len() >= s2.len() { s1 } else { s2 }
}

Lifetime annotations are descriptive, not prescriptive. They describe how long references already live. They do not change anything.

Mistake: Unnecessary Lifetime Annotations

// This is fine — no annotation needed (Rule 2)
fn first_char(s: &str) -> &str {
    &s[..1]
}

// Don't add lifetimes when the compiler doesn't ask:
fn first_char<'a>(s: &'a str) -> &'a str {  // Works but unnecessary
    &s[..1]
}

Only add lifetime annotations when the compiler asks for them. If elision rules handle it, let them.

Lifetimes in Enum Variants

Enums can also hold references with lifetimes:

#[derive(Debug)]
enum Token<'a> {
    Word(&'a str),
    Number(i64),
    Whitespace,
}

fn tokenize(input: &str) -> Vec<Token> {
    let mut tokens = Vec::new();
    for word in input.split_whitespace() {
        if let Ok(num) = word.parse::<i64>() {
            tokens.push(Token::Number(num));
        } else {
            tokens.push(Token::Word(word));
        }
    }
    tokens
}

fn main() {
    let input = String::from("hello 42 world");
    let tokens = tokenize(&input);
    println!("{:?}", tokens);
    // [Word("hello"), Number(42), Word("world")]
}

The Token::Word variant borrows from the input string. The tokens cannot outlive the input.

When Lifetimes Feel Hard

If you find yourself fighting the borrow checker with lifetimes, consider these alternatives:

  1. Return an owned value instead of a reference:
// Instead of this (needs lifetimes):
fn get_name<'a>(user: &'a User) -> &'a str { &user.name }

// Consider this (no lifetimes needed):
fn get_name(user: &User) -> String { user.name.clone() }
  1. Use String instead of &str in structs:
// Instead of this (needs lifetime):
struct Excerpt<'a> { text: &'a str }

// Consider this (no lifetime):
struct Excerpt { text: String }
  1. Use Rc or Arc for shared ownership when references get complicated.

Owned values are simpler. References are faster. Choose based on your needs. For most applications, the performance difference is tiny. Start with owned values and optimize later if needed.

Summary

ConceptSyntaxPurpose
Lifetime annotation'aName a lifetime
Function lifetimefn foo<'a>(x: &'a str) -> &'a strLink input/output lifetimes
Struct lifetimestruct S<'a> { x: &'a str }Struct holds a reference
Static lifetime'staticLives for entire program
Elision Rule 1Each param gets own lifetimeAutomatic
Elision Rule 2One input → all outputsAutomatic
Elision Rule 3&self → all outputsAutomatic

Source Code

View source code on GitHub ->

What’s Next?

We have now covered the complete ownership system: ownership, borrowing, and lifetimes. Next, we learn closures and iterators — two features that make Rust feel like a modern, expressive language. Closures are anonymous functions you can pass around. Iterators let you process collections in a clean, efficient way.

Next: Rust Tutorial #12: Closures and Iterators