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:
// 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
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.
Rule 1 — Each reference parameter gets its own lifetime.
fn f(x: &str)becomesfn f<'a>(x: &'a str). Two parameters get two independent lifetimes:fn f(x: &str, y: &str)becomesfn f<'a, 'b>(x: &'a str, y: &'b str).Rule 2 — If there is exactly one input lifetime, it is applied to all output references.
fn f(x: &str) -> &strbecomesfn f<'a>(x: &'a str) -> &'a str. This is the most commonly triggered rule.Rule 3 — If one of the input parameters is
&selfor&mut self, its lifetime is applied to all output references. This rule makes method syntax ergonomic — you rarely need to annotate method return lifetimes.
Rule 1 in Action
// 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
// 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.
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.
// 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
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.
// 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
'_ 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:
// 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.
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.
// 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 |