How to Read Lifetime Annotations in Rust

Read Rust lifetime annotations by matching the 'a label on input references to the output to ensure data validity.

You write a helper to pick the longer string. The compiler screams.

You write a function that takes two string slices and returns the longer one. The logic is one line. You compile. The compiler rejects you with a wall of text about missing lifetime specifiers. You stare at the code. It looks fine. The strings exist. The function returns one of them. Why does Rust think you're returning a dangling pointer?

The answer isn't magic. It's a contract you haven't written yet. Rust refuses to guess how long a returned reference stays valid. You must tell the compiler which input the output is tied to. Lifetime annotations are that contract. They label references so the borrow checker can verify that data never vanishes while a reference points to it.

The lease analogy

A lifetime is a scope. It tracks how long a reference stays valid. Rust needs to know that every reference points to data that exists for at least as long as the reference itself. If you return a reference from a function, the compiler asks: "How long does this reference live?" Without an answer, Rust assumes the worst. It assumes the data might vanish the moment the function returns.

Think of a lifetime like a lease on an apartment. The reference is the key. The data is the apartment. The lifetime is the lease duration. If you hand someone a key, you need to guarantee the lease covers the time they plan to live there. If the lease expires while they're still inside, you have a problem. Rust's borrow checker enforces that the lease never expires prematurely.

Lifetimes are promises, not timers. The compiler checks the promise, not the clock.

Minimal example

The classic example is a function that returns one of its arguments.

/// Returns the longer of two string slices.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    // 'a ties the return value to the inputs.
    // The result lives as long as both x and y.
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

The label 'a is a lifetime parameter. It appears in three places: on x, on y, and on the return type. This tells the compiler that the returned reference lives as long as 'a, and both x and y also live as long as 'a. The constraint is simple. The output cannot outlive the inputs it references.

The label binds the output to the input. If the input dies, the output dies with it.

Walkthrough

When you write longest<'a>, you're defining a generic lifetime parameter. Just like T in Vec<T>, 'a is a placeholder. The caller fills in the actual scope.

If you call longest with two string literals, the compiler sees they live forever. It substitutes 'a with 'static. If you call it with local variables, the compiler calculates the intersection of their scopes. The return value gets the smaller of the two scopes. This ensures the result never outlives either input.

The compiler verifies this constraint before generating code. It substitutes the lifetimes and checks every borrow against the scope. If a borrow extends past the end of a scope, compilation fails. There is no runtime cost. Lifetimes are erased during compilation.

Lifetimes vanish at compile time. There is zero runtime overhead for these checks.

Parameters versus actual lifetimes

The label 'a is a lifetime parameter. It's a variable name. The actual lifetime is the scope the caller provides. This distinction matters when you chain functions.

When you call longest, the compiler calculates the actual lifetime. It looks at the scopes of the arguments. It picks the smaller of the two scopes. The return value gets that smaller scope. This ensures the result never outlives either input.

The parameter 'a allows the function to be generic over lifetimes. It works for short scopes and long scopes alike. The caller decides the duration. The function promises to respect it.

The parameter is the contract. The caller provides the duration.

Reading the invisible lifetimes

You won't always see lifetime labels in Rust code. The compiler applies elision rules to fill in the gaps. This makes code cleaner but can confuse beginners trying to trace validity.

The rules are mechanical. The compiler applies them in order.

If a function has exactly one input reference, the output reference inherits that input's lifetime. You write fn first_word(s: &str) -> &str. The compiler reads this as fn first_word<'a>(s: &'a str) -> &'a str.

If a function has multiple input references, the compiler cannot guess which one the output is tied to. You must write the annotation. This is why longest needs 'a. The compiler doesn't know if the result comes from x or y.

If the function signature includes &self, the lifetime of self is assigned to all output references. This covers most methods. You write fn level(&self) -> i32. The return type is i32, so no lifetime issue. If you returned &str, it would borrow from self.

Convention aside: Elision is syntactic sugar. The compiler expands it before checking. When you read code without lifetimes, mentally insert the elided labels. It helps you understand the constraints.

Mentally insert the labels when reading code. It reveals the hidden constraints.

Realistic example

Structs often hold references. They must track how long the borrowed data lives.

/// Holds a reference to a piece of text.
struct Excerpt<'a> {
    /// The text slice. 'a ensures the slice is valid.
    part: &'a str,
}

impl<'a> Excerpt<'a> {
    /// Returns a fixed value. No lifetime needed on return.
    fn level(&self) -> i32 {
        3
    }
}

fn main() {
    // String lives in main.
    let novel = String::from("Call me Ishmael.");
    // Slice borrows from novel.
    let first_sentence = novel.split('.').next().unwrap();
    // Excerpt borrows the slice.
    // 'a is inferred to be the scope of first_sentence.
    let e = Excerpt { part: first_sentence };
}

The struct Excerpt has a lifetime parameter 'a. Every instance of Excerpt must be associated with a specific scope. The field part borrows data that lives at least as long as 'a. The impl block repeats the parameter so methods can use it.

Convention aside: In impl blocks, you often see impl<'a> Excerpt<'a>. The lifetime on self in methods is usually elided. You write fn level(&self) instead of fn level<'a>(&'a self). The compiler infers the lifetime of self automatically. This keeps the noise down.

Structs with references must carry the lifetime. The struct cannot outlive its data.

Pitfalls and compiler errors

The most common mistake is returning a reference to a local variable.

fn danger() -> &str {
    let s = String::from("hello");
    // ERROR: s is dropped here. Reference dangles.
    &s
}

The compiler rejects this with E0515 (cannot return reference to local variable). The variable s is dropped at the end of the function. The reference would point to freed memory. Lifetimes can't save you here. There is no input reference to tie the output to. The data dies with the function.

If you try to use a reference after its scope ends, the compiler catches it.

fn main() {
    let result;
    {
        let string1 = String::from("long string");
        result = longest(string1.as_str(), "xyz");
    }
    println!("The longest string is {}", result);
}

This fails. string1 is dropped when the inner block ends. result tries to live beyond that. The compiler enforces the lifetime bound. You can't cheat the scope.

If you see "lifetime may not live long enough", the compiler found a path where the output reference could outlive an input. Check your annotations. Did you tie the output to a reference that might be dropped too early?

If you see "missing lifetime specifier", the compiler needs help. You likely have multiple input references and a returned reference. Add the label.

Convention aside: Read the error code. E0106 is missing lifetime specifier. E0597 is borrowed value does not live long enough. Knowing the code helps you search for solutions.

If you can't prove the data lives, don't return a reference. Return an owned value.

Decision: when to use lifetimes

Use lifetime annotations when a function returns a reference derived from one of its inputs. The compiler needs the label to know which input the output is tied to.

Use lifetime annotations on structs that hold references. The struct must track how long the borrowed data lives.

Use lifetime elision when the pattern is simple. If you have one input reference and return a reference, the compiler assumes the output lifetime matches the input. No annotation needed.

Use owned types like String when the data needs to live longer than the borrow or when multiple owners are required. Returning an owned value avoids lifetime complexity entirely.

Use the 'static lifetime when the data lives for the entire duration of the program, such as string literals or static variables.

Prefer ownership when in doubt. Lifetimes are for borrowing, not for avoiding allocation.

Where to go next