What Are Rust Lifetime Elision Rules? Explained with Examples
Rust lifetime elision is a set of three deterministic rules that the Rust compiler applies to function signatures so that you don't have to write explicit lifetime annotations in common cases. The compiler doesn't guess or infer. It applies the rules mechanically, and if the rules don't produce a complete answer, it rejects the code and asks you to annotate manually. Understanding these three rules lets you predict exactly when you need 'a annotations and when you can omit them.
The Three Lifetime Elision Rules
Rule 1: Each reference parameter gets its own lifetime. A function with one reference parameter gets one lifetime, a function with two gets two, and so on.
Rule 2: If there's exactly one input lifetime, that lifetime is assigned to all output references.
Rule 3: If one of the parameters is &self or &mut self, the lifetime of self is assigned to all output references.
The compiler applies these rules in order. If after all three rules every output reference has a lifetime, elision succeeds. If any output lifetime is still ambiguous, compilation fails.
Rule 2 in Action: One Input Reference
This is the most common case. When a function takes a single reference and returns a reference, the compiler knows that the output must borrow from the only input available.
// What you write — no explicit lifetimes needed.
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[..i];
}
}
s
}
// What the compiler desugars it to after applying Rules 1 and 2.
// fn first_word<'a>(s: &'a str) -> &'a str
Rule 3 in Action: Methods with &self
Methods on structs almost never need explicit lifetimes because Rule 3 ties the output lifetime to self. This keeps method chains ergonomic.
struct Config {
name: String,
env: String,
}
impl Config {
// Rule 3 applies: output lifetime binds to &self.
fn name(&self) -> &str {
&self.name
}
// Even with a second reference param, Rule 3 still picks &self.
fn name_or(&self, _fallback: &str) -> &str {
&self.name
}
// Desugared: fn name_or<'a, 'b>(&'a self, _fallback: &'b str) -> &'a str
}
When Elision Fails: Two Input References, No &self
The compiler applies Rule 1 (assign separate lifetimes to each input) and then tries Rule 2. Rule 2 requires exactly one input lifetime, and here there are two. Rule 3 doesn't apply because there's no self. The compiler gives up and asks you to annotate.
// This does NOT compile — elision can't determine the output lifetime.
// fn longest(a: &str, b: &str) -> &str { ... }
// You must annotate explicitly.
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() {
a
} else {
b
}
}
Gotcha: Rule 3 Can Mislead You
Rule 3 always ties the output to &self, even when the returned reference actually borrows from a different parameter. This compiles but might not express the relationship you intend. If the returned value derives from a non-self parameter, you need to override elision with explicit annotations so that the borrow checker enforces the correct constraint.
struct Parser {
delimiter: char,
}
impl Parser {
// Elision ties output to &self, but we actually return from input.
// This is fine if we truly return from input that outlives self,
// but the caller gets the wrong constraint.
// fn parse(&self, input: &str) -> &str // output tied to self, NOT input
// Correct: explicitly tie output to input.
fn parse<'a>(&self, input: &'a str) -> &'a str {
input.split(self.delimiter).next().unwrap_or(input)
}
}
Elision in impl Blocks and Trait Objects
Since Rust 2018, lifetime elision extends beyond function signatures. You can elide lifetimes on impl blocks for types that contain references. dyn Trait objects get a default lifetime bound of 'static when owned and the enclosing reference's lifetime when behind a reference.
struct Wrapper<'a> {
data: &'a str,
}
// Elided: you don't have to write impl<'a> Wrapper<'a> in many contexts.
impl Wrapper<'_> {
fn get(&self) -> &str {
self.data
}
}
fn main() {
let text = String::from("hello world");
let w = Wrapper { data: &text };
println!("{}", w.get());
}
Quick Mental Model for Lifetime Elision
When you see a function signature with references and no lifetime annotations, walk through the three rules mentally. If the function has one input reference, elision works (Rule 2). If it's a method on &self, elision works (Rule 3). If it has multiple input references and no self, elision fails and you need explicit annotations. That covers roughly 95% of the cases you'll encounter.
One final pitfall: closures don't use the same elision rules as named functions. A closure like |s: &str| -> &str { s } fails to compile even though the equivalent named function works fine. This is a known ergonomic gap in the language. If you hit this, extract the closure into a named function or annotate the closure's types with a helper trait bound.