RustLifetime Elision

Lifetime Elision in Rust

If every reference in Rust required an explicit lifetime annotation, code would become painfully verbose. Rust solves this with lifetime elision — a set of deterministic rules the compiler applies to infer lifetimes in common patterns, so you can omit them without losing safety guarantees.

Elision is not guessing. The compiler follows exact rules, and when those rules produce an unambiguous result, no annotation is needed. When they do not, the compiler asks you to be explicit.

What Elision Means in Practice

Consider first_word, a function that returns a slice of its input string. Both the verbose and elided forms are identical to the compiler:

RUST
// Explicit — you write the lifetime yourself
fn first_word<'a>(s: &'a str) -> &'a str {
    s.split_whitespace().next().unwrap_or("")
}

// Elided — the compiler fills in the same annotation automatically
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

fn main() {
    let sentence = String::from("hello world");
    let word = first_word(&sentence);
    println!("first word: {}", word);
}
first word: hello
Note
The two versions of first_word compile to identical code. Elision is purely a syntactic convenience — the borrow checker still works with fully-annotated lifetimes internally.
The Three Elision Rules

The compiler applies three rules in order when it encounters a function signature with references and no explicit lifetime annotations. Input lifetimes are on parameters; output lifetimes are on the return type.

  1. Rule 1 — Each reference parameter gets its own lifetime. fn f(x: &str) becomes fn f<'a>(x: &'a str). Two parameters get two independent lifetimes: fn f(x: &str, y: &str) becomes fn f<'a, 'b>(x: &'a str, y: &'b str).

  2. Rule 2 — If there is exactly one input lifetime, it is applied to all output references. fn f(x: &str) -> &str becomes fn f<'a>(x: &'a str) -> &'a str. This is the most commonly triggered rule.

  3. Rule 3 — If one of the input parameters is &self or &mut self, its lifetime is applied to all output references. This rule makes method syntax ergonomic — you rarely need to annotate method return lifetimes.

Tip
If all three rules are applied and output lifetimes are still ambiguous, the compiler reports an error and asks you to add explicit annotations.
Rule 1 in Action

RUST
// Written by you (elided):
fn describe(x: &str, y: &str) -> String {
    format!("{} and {}", x, y)
}

// What the compiler sees after Rule 1:
fn describe<'a, 'b>(x: &'a str, y: &'b str) -> String {
    format!("{} and {}", x, y)
}

// No Rule 2 or 3 needed — the return type is String (owned), not a reference.
Rule 2 in Action

RUST
// Written by you (elided):
fn trim_prefix<'_>(s: &str) -> &str {
    s.trim_start_matches("prefix_")
}

// Step 1 — Rule 1 assigns a lifetime to the single input:
//   fn trim_prefix<'a>(s: &'a str) -> &str
// Step 2 — Rule 2 sees exactly one input lifetime; applies it to the output:
//   fn trim_prefix<'a>(s: &'a str) -> &'a str

fn main() {
    let name = String::from("prefix_hello");
    let trimmed = trim_prefix(&name);
    println!("{}", trimmed);
}
hello
Rule 3 in Action: Methods with &self

Rule 3 is what makes method return values ergonomic. When a method returns a reference and takes &self, the returned reference is assumed to borrow from self — which is almost always what you want.

RUST
struct Config {
    prefix: String,
}

impl Config {
    // Elided — Rule 3 gives the return value self's lifetime
    fn prefix(&self) -> &str {
        &self.prefix
    }

    // Equivalent explicit form:
    // fn prefix<'a>(&'a self) -> &'a str { &self.prefix }

    // With an extra parameter — Rule 3 still applies to the return value
    fn prefix_or<'a>(&'a self, fallback: &'a str) -> &'a str {
        if self.prefix.is_empty() { fallback } else { &self.prefix }
    }
}

fn main() {
    let cfg = Config { prefix: String::from("app_") };
    println!("prefix: {}", cfg.prefix());
    println!("or: {}", cfg.prefix_or("default_"));
}
prefix: app_
or: app_
When Elision Fails — You Must Be Explicit

Elision only works when the rules produce an unambiguous result. The most common case where they fail is a function with multiple input references and a reference return type, where the return could come from either input.

RUST
// This does NOT compile — ambiguous which input the return borrows from
// fn choose(x: &str, y: &str) -> &str { x }

// You must annotate — tell the compiler the return borrows from x
fn choose<'a>(x: &'a str, y: &str) -> &'a str {
    let _ = y;
    x
}

fn main() {
    let a = String::from("alpha");
    let result;
    {
        let b = String::from("beta");
        result = choose(&a, &b);
        // b can be dropped here — return lifetime only tied to a
    }
    println!("chosen: {}", result);
}
chosen: alpha
Warning
When you see the error "missing lifetime specifier," it means none of the three elision rules resolved the output lifetime. Read the function signature: which input does the return value borrow from? Annotate that relationship explicitly.
The '_ Anonymous Lifetime

Rust offers a shorthand called the anonymous lifetime or placeholder lifetime, written '_. It tells the compiler "infer this lifetime — I acknowledge a lifetime exists here but do not need to name it." It is most useful in impl blocks and type aliases to avoid cluttering names.

RUST
// Without '_ — you must name the lifetime in the impl line
impl<'a> std::fmt::Display for Important<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.part)
    }
}

// With '_ — cleaner when you do not need to reference the lifetime elsewhere
impl std::fmt::Display for Important<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.part)
    }
}

struct Important<'a> {
    part: &'a str,
}

fn main() {
    let text = String::from("hello lifetime");
    let imp = Important { part: &text };
    println!("{}", imp);
}
hello lifetime
Tip
'_ says "there is a lifetime here — figure it out from context." It is not the same as omitting the annotation; it explicitly acknowledges that a reference lifetime exists.
Reading Elided Lifetimes in Other People's Code

When reading Rust code in the wild, you will encounter elided lifetimes constantly. Here is how to mentally expand them using the three rules:

RUST
// What you read:
fn split_at_comma(s: &str) -> (&str, &str) {
    match s.find(',') {
        Some(i) => (&s[..i], &s[i + 1..]),
        None    => (s, ""),
    }
}

// What the compiler sees (Rule 1 then Rule 2):
fn split_at_comma<'a>(s: &'a str) -> (&'a str, &'a str) {
    match s.find(',') {
        Some(i) => (&s[..i], &s[i + 1..]),
        None    => (s, ""),
    }
}

fn main() {
    let data = String::from("name,value");
    let (key, val) = split_at_comma(&data);
    println!("key={} val={}", key, val);
}
key=name val=value
Elision in Struct impl Blocks

In impl blocks the lifetime is declared once and used throughout, but methods that return &self data benefit from Rule 3 and need no extra annotation.

RUST
struct Sentence<'a> {
    text: &'a str,
}

impl<'a> Sentence<'a> {
    // Rule 3: &self's lifetime ('a) applied to return value
    fn text(&self) -> &str {
        self.text
    }

    // Must be explicit — returns a sub-slice; could come from either input
    fn longest_word(&self, other: &'a str) -> &'a str {
        let self_word = self.text.split_whitespace()
            .max_by_key(|w| w.len())
            .unwrap_or("");
        if self_word.len() >= other.len() { self_word } else { other }
    }
}

fn main() {
    let raw = String::from("the quick brown fox");
    let sentence = Sentence { text: &raw };
    println!("text: {}", sentence.text());
    println!("longest: {}", sentence.longest_word("elephant"));
}
text: the quick brown fox
longest: elephant
The 'static Lifetime and When It Is Required

'static is the longest possible lifetime — the entire duration of the program. String literals automatically have 'static, but owned data that you want to share across threads also often ends up requiring 'static because threads may outlive the current scope.

RUST
// String literals are &'static str
const GREETING: &str = "hello";   // implicitly &'static str

// Thread closures require 'static because the thread may outlive the spawner
fn spawn_with_message(msg: &'static str) {
    std::thread::spawn(move || {
        println!("thread says: {}", msg);
    }).join().unwrap();
}

fn main() {
    spawn_with_message("this is a string literal");
    // spawn_with_message(&String::from("owned")); // ERROR — not 'static
}
thread says: this is a string literal
Elision Rules at a Glance

Rule

Trigger

Effect

Rule 1

Any reference parameter

Each &T gets its own lifetime: &'a T, &'b U, ...

Rule 2

Exactly one input lifetime

Output references share that single input lifetime

Rule 3

&self or &mut self in method

Output references share self's lifetime

None match

Multiple inputs, no &self, ambiguous output

Compiler error — explicit annotation required

Success
Lifetime elision exists to make everyday Rust feel natural. Most functions that take a single reference and return a reference "just work" without annotations. You only reach for explicit lifetimes when writing genuinely complex borrowing relationships — and at that point the annotations serve as precise documentation.