How to Annotate Lifetimes on Functions in Rust

Add a lifetime parameter like 'a to the function signature and apply it to input and output references to ensure the returned reference is valid.

When the compiler asks for a contract

You write a function that takes two string slices and returns the longer one. The logic is trivial. You compile, and the build fails with E0106: missing lifetime specifier. You slap 'a on every reference in the signature. The error vanishes. You feel like you cast a spell, but you don't know what the spell does.

This is the rite of passage for every Rust developer. Lifetime annotations look like cryptic syntax, but they are actually precise contracts. You are teaching the compiler how references flow through your function so it can guarantee the returned reference never dangles. When you annotate a lifetime, you aren't changing runtime behavior. You are giving the compiler the information it needs to prove your code is safe.

The contract in plain words

A lifetime is a scope. A reference is valid only within that scope. When a function takes references and returns a reference, the compiler needs to know how the output scope relates to the input scopes. Does the output borrow from the first argument? The second? Both? The annotation defines this relationship.

Think of it like a lease agreement. The caller hands you a key and says, "This apartment is available for duration 'a." You promise, "The key I give back will also work for duration 'a." If the caller tries to hand you a key for a pop-up shop that closes in five minutes, but you promised a year-long lease, the contract is broken. The compiler rejects the call.

The annotation isn't metadata. It's a constraint the compiler enforces. If you lie in the annotation, the compiler will catch you when you try to use the function.

Minimal example

The classic example is a function that returns the longer of two strings.

/// Returns a reference to the longer of two string slices.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    // The 'a ties the output lifetime to the input lifetimes.
    // The compiler ensures the returned reference is valid as long as both inputs.
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

The syntax breaks down into three parts. The <'a> after the function name declares a generic lifetime parameter. The &'a str on the arguments applies that parameter to the references. The &'a str on the return type ties the output to the same parameter.

This tells the compiler: "I take two references that live for at least 'a, and I return a reference that also lives for at least 'a." The compiler infers 'a by looking at the actual arguments passed at the call site. It picks the shortest lifetime that satisfies all constraints. If you pass a reference that lives for the whole program and a reference that lives for one block, the inferred 'a becomes the one-block duration. The returned reference is restricted to that block. This prevents you from holding onto a reference after the shorter-lived data is dropped.

What the compiler checks

When you call longest, the compiler performs a few checks. It verifies that both arguments live long enough to satisfy the inferred 'a. It ensures the return value doesn't outlive 'a. If you try to use the result after one of the inputs goes out of scope, the compiler emits an error.

fn main() {
    let string1 = String::from("long string");
    let result;

    {
        let string2 = String::from("xyz");
        // string2 is dropped at the end of this block.
        // result borrows from string2, so it can't live past this block.
        result = longest(&string1, &string2);
    }

    // E0505: attempt to use `result` after `string2` was dropped.
    println!("The longest string is {}", result);
}

The error here isn't about the annotation. The annotation is correct. The error is about usage. You tried to use a reference after the data it points to was dropped. The lifetime system caught the bug at compile time.

Realistic example: slicing and borrowing

Lifetimes come up naturally when you slice strings or work with collections. Consider a function that extracts the first word from a sentence.

/// Extracts the first word from a sentence.
fn first_word<'a>(s: &'a str) -> &'a str {
    // Find the byte index of the first space.
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            // Return the slice up to the space.
            // The slice borrows from s, so it shares s's lifetime.
            return &s[..i];
        }
    }
    // If no space, return the whole string.
    s
}

Here, the output depends entirely on the input. The slice &s[..i] borrows data from s. The annotation &'a str on the return type tells the compiler the slice can't outlive s. This is straightforward because there's only one input reference. The compiler could actually infer this without the annotation using lifetime elision rules, but writing it explicitly clarifies the relationship.

Convention aside: the community uses 'a for the first lifetime parameter. If you need a second, use 'b. For complex functions with many references, descriptive names like 'input or 'config improve readability. Don't use names like 'life or 'duration; they imply the annotation changes the lifetime, which it doesn't.

Pitfalls and errors

Over-constraining is the most common mistake. When you write fn foo<'a>(x: &'a str, y: &'a str) -> &'a str, you are forcing x and y to share the same lifetime. If the return value only depends on x, you are unnecessarily restricting y. This breaks valid code where y is a temporary value that dies before x.

// BAD: Forces x and y to have the same lifetime.
fn first_arg_bad<'a>(x: &'a str, y: &'a str) -> &'a str {
    x
}

// GOOD: y can have a different lifetime. Only x matters for the return.
fn first_arg_good<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    x
}

fn main() {
    let x = String::from("long string");
    let result;
    {
        let y = String::from("short");
        // This fails with first_arg_bad because y must live as long as result.
        // result = first_arg_bad(&x, &y); // Error!
        
        // This works with first_arg_good.
        result = first_arg_good(&x, &y);
    }
    println!("{}", result);
}

The compiler allows the bad version, but your API becomes harder to use. Other developers will hit borrow errors when they try to pass a short-lived value for y. Always use the minimum constraints required by the logic. If the return value only borrows from x, let y have its own lifetime.

Another trap is thinking annotations create lifetimes. They don't. A lifetime annotation is a label. It groups references together. It does not make data live longer. If you try to return a reference to a local variable, adding 'a won't help. The compiler knows the local variable dies when the function returns. It will emit E0515: cannot return value referencing local variable.

fn invalid<'a>() -> &'a str {
    let s = String::from("hello");
    // E0515: cannot return value referencing local variable `s`
    &s
}

No amount of annotation syntax can fix a logic error where you try to hand out a reference to data that vanishes.

Lifetimes describe relationships; they don't extend the life of data. You can't annotate a dangling reference into safety.

Decision matrix

Use explicit lifetime annotations when a function returns a reference and the compiler cannot infer which input the output borrows from. Use multiple lifetime parameters when the return value depends on only a subset of the inputs, so you can allow other arguments to have shorter, independent lifetimes. Use lifetime elision (no annotations) when the function has exactly one input reference or a &self receiver; the compiler applies standard rules to fill in the gaps. Use owned types like String when the function creates new data or must return a value that outlives all inputs. Use &'static str for string literals that are baked into the binary and live for the entire program duration.

Where to go next