What Is Lifetime Elision and When Does It Apply?

Lifetime elision is a compiler feature that automatically infers reference lifetimes in Rust functions, reducing the need for explicit annotations.

When the compiler fills in the blanks

You write a function to extract the first word from a sentence. You pass in a string slice and return a string slice. The code compiles instantly. You look at the signature and see no lifetime annotations.

Then you write a function that takes two string slices and returns the longer one. The compiler rejects it with a missing lifetime specifier error. You add <'a> to the signature, and it works.

The difference is lifetime elision. Rust includes a set of rules that let the compiler infer lifetime parameters for references when you don't write them explicitly. Elision removes boilerplate for common patterns while keeping the borrow checker fully informed. When the relationship between inputs and outputs is unambiguous, the compiler fills in the lifetimes for you. When the relationship is ambiguous, the compiler forces you to be explicit.

Elision is a shorthand for obvious lifetimes

Lifetime elision is not a magic feature that changes how borrowing works. It is a syntactic convenience. The compiler applies a fixed set of heuristics to function and method signatures. If the signature matches a heuristic, the compiler inserts the lifetime parameters automatically. If it does not match, the compiler stops and asks you to provide them.

Think of elision like a form with pre-filled fields. If you provide the essential information, the clerk assumes the rest based on standard practice. If you provide conflicting information, the clerk hands the form back and asks you to clarify.

The goal is to make the common case easy to write while preserving safety. Most functions that return references derive those references from a single input. Elision captures this pattern so you don't have to annotate every simple function.

Minimal example: one input, one output

Consider a function that finds the first word in a string. It takes a &str and returns a &str. The output reference points into the input string. There is only one input reference, so the compiler knows exactly where the output must borrow from.

/// Returns the first word of a string slice.
fn first_word(s: &str) -> &str {
    // The compiler sees one input reference and one output reference.
    // It applies the elision rule for single inputs: the output borrows for the same duration as the input.
    let bytes = s.as_bytes();
    // Iterate over bytes to find the first space character.
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    // No space found, return the entire string.
    &s[..]
}

The compiler infers the signature as fn first_word<'a>(s: &'a str) -> &'a str. You get the safety of explicit lifetimes without the syntax noise. The borrow checker sees the inferred lifetimes and enforces the rules exactly as if you had written them.

The code works because the compiler filled in the invisible lifetime parameter. You get the safety without the syntax noise.

How the rules work

The compiler follows three patterns to infer lifetimes. These rules apply to functions and methods. They do not apply to trait definitions or generic type parameters.

The compiler assigns a unique lifetime parameter to each input reference. If the function has exactly one input lifetime, the compiler assigns that lifetime to all output references. If the function has multiple input references but one of them is &self or &mut self, the compiler assigns the lifetime of self to all output references.

The rules are checked in order. The compiler looks for a single input lifetime first. If that fails, it looks for &self. If neither applies, elision fails and the compiler requires explicit annotations.

These rules capture the most common safe patterns. If your code fits a pattern, the compiler saves you the typing.

Realistic example: when elision fails

Elision fails when the compiler cannot determine which input reference the output borrows from. This happens when a function takes multiple input references and returns a reference that could come from any of them.

/// Returns the longer of two string slices.
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

The compiler rejects this with a missing lifetime specifier error. There are two input references, x and y. The output could borrow from x or from y. The compiler does not know which lifetime to assign to the return value. It cannot assume the output borrows from both, because that would be too restrictive. It cannot assume the output borrows from one arbitrarily, because that would be unsafe.

You must provide explicit lifetime annotations to document the relationship.

/// Returns the longer of two string slices.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

The annotation <'a> declares a lifetime parameter. The signature says the returned reference lives at least as long as both x and y. This is a valid contract because the function returns one of the inputs. The borrow checker enforces this contract at call sites.

When elision fails, the compiler isn't being difficult. It's protecting you from a bug where the returned reference could dangle if the wrong input expires first.

Pitfalls and edge cases

Elision has boundaries. Understanding where elision stops helps you write correct signatures and interpret compiler errors.

Elision does not apply to trait definitions. Trait methods require explicit lifetime annotations. This is intentional. Traits define a contract that multiple types must implement. Explicit lifetimes document the contract clearly for all implementors. If you write a trait method with references, you must annotate the lifetimes.

/// A trait that compares string slices.
trait Compare {
    // Explicit lifetimes are required here.
    // Elision does not apply to trait definitions.
    fn longest<'a>(&self, other: &'a str) -> &'a str;
}

Elision does not change the safety rules. You cannot use elision to return a reference to a local variable. The compiler infers lifetimes based on inputs. If there are no inputs, there is no lifetime to infer. The function cannot return a reference.

/// Attempts to return a reference to a local string.
fn dangerous() -> &str {
    let s = String::from("hello");
    // Error: missing lifetime specifier.
    // The compiler cannot infer a lifetime because there are no inputs.
    &s
}

The compiler rejects this with a missing lifetime specifier error. Even if you add a lifetime parameter, the borrow checker will reject the function because s is dropped at the end of the function. Elision is syntactic sugar. It never allows unsafe code.

Community convention favors elided lifetimes whenever possible. Explicit lifetimes are used only when necessary. When you do write explicit lifetimes, name them 'a, 'b, etc. Convention uses 'a for the first lifetime parameter. Some developers use descriptive names like 'input or 'output for complex functions, but 'a remains the standard for most code.

Trust the error message. If the compiler asks for a lifetime, it means the relationship between inputs and outputs is too complex to guess safely.

Decision: elided vs explicit lifetimes

Choose the annotation style based on the data flow in your function.

Use elided lifetimes when your function signature contains exactly one input reference and returns a reference. Use elided lifetimes when writing a method that takes &self or &mut self and returns a reference. Use explicit lifetimes when a function accepts multiple input references and returns a reference that could originate from any of them. Use explicit lifetimes when the returned reference has a lifetime shorter than the inputs, such as when returning a reference to a value computed inside the function. Use explicit lifetimes when defining traits, because trait definitions require explicit lifetime annotations to establish the contract for all implementors.

Write the signature that matches the data flow. If the flow is simple, let elision handle it. If the flow is complex, spell it out.

Where to go next