The three rules Rust uses to infer lifetimes automatically, so you understand when you need to annotate and when the compiler handles it for you.
Rust has explicit lifetime syntax ('a, 'b) but you don't write it most of the time. The compiler applies three elision rules in order and only forces you to annotate when it can't figure it out.
Rule 1: Each parameter that is a reference gets its own lifetime.
fn foo(x: &str, y: &str)
// becomes:
fn foo<'a, 'b>(x: &'a str, y: &'b str)Rule 2: If there's exactly one input lifetime, it's assigned to all output lifetimes.
fn first_word(s: &str) -> &str
// becomes:
fn first_word<'a>(s: &'a str) -> &'a strThis is why first_word compiles without annotations — rule 2 handles it.
Rule 3: If there's a &self or &mut self parameter, its lifetime is assigned to all output lifetimes.
impl Config {
fn description(&self) -> &str {
&self.desc
}
}
// The returned &str has the same lifetime as &self — compiler knows this.If none of the three rules produce an unambiguous result, you annotate.
// Two input references, no &self — rule 2 doesn't apply, ambiguous output
fn longer<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}The annotation 'a here says: the returned reference lives at least as long as the shorter of x and y. The compiler can't infer this from the rules alone.
If you're writing a function that returns a reference and the compiler complains, ask: does the returned reference come from self, from one specific input, or is it ambiguous? If ambiguous, annotate. The annotation isn't for the compiler's benefit — it's a contract you're making explicit.
Karanveer Singh Shaktawat
Full Stack Engineer & Infrastructure Architect
I build production systems across web, mobile, and infrastructure — then document what went wrong and why.
Pick what you want to hear about — I'll only email when it's worth it.
Did this resonate?
The difference between static dispatch (impl Trait) and dynamic dispatch (dyn Trait) — not just syntax, but a real tradeoff.
How to use Docker multi-stage builds to go from a 2GB Rust build image to a 12MB production image.
Building Docsee — a cross-platform Docker management tool with a Tauri GUI and terminal TUI, and why Rust was the right choice.