TechLead
Lesson 5 of 28
5 min read
Rust

Lifetimes

Understand Rust lifetime annotations, elision rules, lifetimes in structs, 'static lifetime, and how the borrow checker uses lifetimes.

What Are Lifetimes?

Lifetimes are Rust's way of ensuring that references are always valid. Every reference in Rust has a lifetime — the scope for which that reference is valid. Most of the time, lifetimes are inferred automatically. When they can't be, you add lifetime annotations to tell the compiler how references relate to each other.

Key Insight

Lifetime annotations don't change how long references live. They describe the relationships between lifetimes of multiple references so the borrow checker can verify safety. Think of them as contracts, not commands.

Why the Borrow Checker Needs Lifetimes

// This function returns a reference, but to WHICH input?
// The compiler can't tell without a lifetime annotation.

// Won't compile without lifetimes:
// fn longest(x: &str, y: &str) -> &str {
//     if x.len() > y.len() { x } else { y }
// }

// With lifetime annotations:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
// 'a means: the returned reference lives at least as long
// as the SHORTER of x and y's lifetimes

fn main() {
    let string1 = String::from("long string");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
        println!("Longest: {result}"); // OK: both alive here
    }
    // println!("{result}"); // ERROR if result could be string2
    // The compiler enforces that result doesn't outlive string2
}

Lifetime Annotation Syntax

// Lifetime parameters start with ' and are usually short
// Convention: 'a, 'b, 'c, etc.

&i32        // a reference (lifetime is inferred)
&'a i32     // a reference with an explicit lifetime 'a
&'a mut i32 // a mutable reference with lifetime 'a

// Multiple lifetime parameters
fn multi<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    // Return type tied to 'a, so only x can be returned
    x
}

// The return type's lifetime must match one of the input lifetimes
// (or be 'static). You can't return a reference to local data.

Lifetime Elision Rules

The compiler applies three elision rules to infer lifetimes automatically. If all references can be resolved by these rules, you don't need annotations.

  1. Each reference parameter gets its own lifetime.
    fn 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 references.
    fn foo(x: &str) -> &str becomes fn foo<'a>(x: &'a str) -> &'a str
  3. If one parameter is &self or &mut self, its lifetime is assigned to all outputs.
    Methods that return references usually borrow from self.
// These are equivalent — the compiler elides the lifetime:
fn first_word(s: &str) -> &str { /* ... */ }
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ }

// Elision works for methods:
struct Parser {
    input: String,
}

impl Parser {
    // &self lifetime is automatically applied to output
    fn peek(&self) -> &str {
        &self.input[..1]
    }
    // Equivalent to: fn peek<'a>(&'a self) -> &'a str
}

Lifetimes in Structs

// Structs that hold references need lifetime annotations
struct Excerpt<'a> {
    part: &'a str,
}

impl<'a> Excerpt<'a> {
    fn level(&self) -> i32 {
        3 // No reference in return — no annotation needed
    }

    fn announce(&self, announcement: &str) -> &str {
        // Elision rule 3: return lifetime = &self lifetime
        println!("Attention: {announcement}");
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel
        .split('.')
        .next()
        .expect("Could not find a '.'");

    let excerpt = Excerpt {
        part: first_sentence,
    };
    println!("Excerpt: {}", excerpt.part);
    // excerpt cannot outlive novel (the data it references)
}

The 'static Lifetime

// 'static means the reference lives for the entire program

// String literals are always 'static
let s: &'static str = "I live forever";

// Common in error types and trait bounds
fn make_error() -> Box {
    Box::new(std::io::Error::new(
        std::io::ErrorKind::Other,
        "something went wrong"
    ))
}

// Be careful: 'static doesn't always mean what you think
// It means "CAN live that long", not "MUST live that long"
fn generic_print(val: T) {
    println!("{val}");
}
// String is 'static (it owns its data, no references)
// &String is NOT 'static (unless the String is leaked)

When to Use 'static

Don't reach for 'static as a quick fix for lifetime errors. Usually it means you should restructure your code to use owned types (String instead of &str) or properly annotate the actual lifetimes.

Key Takeaways

  • ✅ Lifetimes describe how long references are valid relative to each other
  • ✅ The compiler's elision rules handle most cases automatically
  • ✅ Structs holding references must declare lifetime parameters
  • 'static means the reference can live for the entire program duration
  • ✅ Lifetime annotations are contracts — they don't change actual lifetimes

Continue Learning