How to fix lifetime mismatch error

Fix Rust lifetime mismatch errors by adding explicit lifetime annotations to function signatures to link input and output references.

The compiler sees a reference and asks: "How long does this live?"

You write a function to find the largest number in a slice. The logic is solid. You iterate, compare, and return a reference to the winner. You hit run. The compiler rejects the code with error[E0106]: missing lifetime specifier. You stare at the screen. The function works in your head. The connection between the input slice and the output reference is obvious. The compiler says no.

This happens because Rust requires every reference in a function signature to have a lifetime. A lifetime is a label that describes how long a reference stays valid. When a function returns a reference, the compiler needs to know which input reference guarantees the output's validity. If you don't provide that link, the compiler stops. It won't guess when there's ambiguity. The fix is to add a lifetime parameter that ties the input and output together.

Lifetimes are contracts, not timers

A reference is a view into data owned by something else. That data has a scope. When the owner drops the data, the memory is reclaimed. A reference pointing to reclaimed memory is a dangling pointer, which causes crashes. Rust prevents this by tracking lifetimes.

A lifetime is the scope where a reference is valid. Think of it as a promise. When you hand someone a reference, you promise the data will stay alive as long as they hold that reference. The lifetime parameter in a signature is the formal version of that promise. It tells the compiler: "The output reference lives as long as this input reference lives."

Lifetimes do not change how long data lives. They do not extend scopes. They only describe relationships. You cannot annotate a dangling reference into safety. If the data dies inside the function, the reference dies too. The lifetime annotation just helps the compiler enforce that the caller keeps the data alive while using the result.

Minimal example: linking input to output

The error usually appears when a function takes a reference and returns a reference. The compiler sees two references but doesn't know if they are related.

// This fails to compile.
// The compiler sees `list` and the return value as separate lifetimes.
// It assumes the return value might not be tied to `list`.
fn largest(list: &[i32]) -> &i32 {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

The compiler rejects this with E0106. It needs a lifetime parameter. The fix adds 'a to the function, the input, and the output.

// The lifetime parameter `'a` links the input and output.
// It says: "The returned reference lives as long as `list` lives."
fn largest<'a>(list: &'a [i32]) -> &'a i32 {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

The 'a is a type parameter for time. It appears in the signature after the function name. It appears on the input slice and the return type. This tells the compiler that the output is borrowed from the input. If the caller drops list, the compiler will prevent them from using the result.

Walkthrough: what the compiler checks

When you call largest, the compiler checks the scopes.

fn main() {
    let numbers = vec![10, 40, 5];
    // `numbers` is alive. The slice `&numbers` is valid.
    let max = largest(&numbers);
    // `max` is valid because `numbers` is still alive.
    println!("Max: {}", max);
    
    // If you tried to use `max` after `numbers` is dropped,
    // the compiler would reject it.
}

The lifetime 'a is resolved at the call site. The compiler picks a concrete scope for 'a that fits both the argument and the usage. In this case, 'a becomes the scope of numbers. The return value inherits that scope.

If you try to return a reference to data that doesn't come from the input, the lifetime annotation won't help.

// This fails with E0515: cannot return reference to local variable.
// No lifetime annotation can fix this.
fn bad() -> &i32 {
    let x = 5;
    &x
}

The variable x is local. It dies when the function returns. There is no input to tie the output to. The compiler rejects this immediately. You must return owned data, or pass the data in as an argument.

Lifetimes are proof, not magic. You can't prove a reference is valid if the data vanishes.

Realistic example: multiple inputs and ambiguity

The error gets trickier when a function has multiple input references. The compiler doesn't know which input the output is tied to.

// This fails with E0106.
// The output could come from `x` or `y`.
// The compiler refuses to guess.
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

You need to declare the relationship. The output lives as long as the input it came from. Since the function could return either x or y, the output must live as long as the shorter of the two scopes. You tie both inputs to the same lifetime 'a.

// Both inputs share lifetime `'a`.
// The output also has lifetime `'a`.
// This means the output lives as long as both inputs live.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

This signature is conservative. It requires both x and y to live for the same duration 'a. The output is valid for that duration. This is safe. If x lives longer than y, the result is constrained by y's lifetime. The compiler ensures you don't use the result after y drops.

Convention aside: The community prefers lifetime elision when possible. Rust has rules that let the compiler guess lifetimes in common cases. If a function has exactly one input reference, the compiler assigns that lifetime to the output. fn largest(list: &[i32]) -> &i32 would work if the compiler could guess, but the signature has an implicit lifetime on list and an implicit lifetime on the return. The elision rule applies to the annotated signature. In fn largest(list: &[i32]) -> &i32, the compiler sees one input reference and assigns its lifetime to the output. Wait, that's not right. The error E0106 happens because the signature is missing the lifetime parameter. Elision works on the syntax. fn largest(list: &str) -> &str actually compiles due to elision. The compiler infers fn largest<'a>(list: &'a str) -> &'a str.

The error appears when elision fails. Multiple inputs break the single-input rule. Struct methods with &self and other references can break the &self rule. When elision fails, add the explicit lifetime.

Convention aside: Run cargo clippy after adding lifetimes. Clippy has a lint needless_lifetimes that flags annotations you don't need. If clippy suggests removing a lifetime, do it. Elision is preferred for readability. Explicit lifetimes are only for when the compiler needs help.

Pitfalls and compiler errors

Lifetime errors fall into two categories. Signature errors and usage errors.

Signature errors happen when the function definition is incomplete. The compiler emits E0106. The fix is to add lifetime parameters.

Usage errors happen when the caller violates the lifetime contract. The compiler emits E0597 (borrowed value does not live long enough).

fn main() {
    let result;
    {
        let x = String::from("hello");
        // `x` is dropped at the end of this block.
        result = &x;
    }
    // `result` tries to use `x` after it's gone.
    println!("{}", result);
}

The compiler rejects this with E0597. The reference result outlives the data x. You cannot fix this with lifetime annotations. You must change the scope. Move x outside the block, or return an owned String instead of a reference.

Another common pitfall is over-constraining. Adding 'a to every reference when only some are related.

// This ties `config` and `key` to the same lifetime.
// The caller must keep both alive for the same duration.
// This is stricter than necessary.
fn lookup<'a>(config: &'a Config, key: &'a str) -> &'a str {
    // ...
}

If the result only depends on config, you can separate the lifetimes.

// `key` can be a short-lived string literal.
// `config` must live as long as the result.
fn lookup<'a>(config: &'a Config, key: &str) -> &'a str {
    // ...
}

This gives the caller more flexibility. Use separate lifetimes when the inputs have independent validity.

Section closer: Treat the lifetime signature as a contract. If the signature is too strict, callers will struggle. If it's too loose, the compiler won't catch bugs. Match the lifetimes to the actual data flow.

Decision: when to use lifetimes vs alternatives

Use explicit lifetime annotations when the function has multiple input references and the compiler cannot guess which one ties to the output. Use explicit lifetime annotations when a struct holds references, because the struct definition must declare how long those references stay valid. Use lifetime elision when there is exactly one input reference; the compiler automatically assigns that lifetime to the output. Use owned types like String or Vec<T> instead of references when the function needs to create new data or when the caller needs the value to outlive the source. Reach for Cow<T> when you sometimes return borrowed data and sometimes return owned data, avoiding the need to clone in the borrowed case.

Where to go next