Lifetime Elision Rules
Understand the rules that let Rust infer lifetimes automatically so you can write functions without explicit lifetime annotations in most situations
Rust requires every reference to have a known lifetime so the borrow checker can verify memory safety. In the language’s earliest days, you had to write those lifetimes out by hand on every function signature that touched references. That produced code like fn first_word<'a>(s: &'a str) -> &'a str, which works but quickly becomes noise when lifetimes follow predictable patterns. The developers of Rust noticed that the vast majority of real‑world code repeated a small set of borrowing patterns, so they encoded those patterns into the compiler. The result is lifetime elision: a set of deterministic rules the compiler uses to fill in lifetime parameters when you leave them out.
Elision does not relax the borrow checker or change what lifetimes mean. It is purely a convenience that reduces how often you must type 'a. The compiler still assigns concrete lifetimes and checks them; the only difference is that you don’t see them in your source code unless the rules can’t infer a unique answer.
Why Lifetime Elision Exists
Before elision, every function that accepted a reference and returned a reference required an explicit lifetime parameter, even when the relationship was obvious. For instance, a function that takes one string slice and returns a substring of it has only one sensible outcome: the returned reference cannot outlive the input. Writing that lifetime manually dozens of times across a codebase added no safety — it was pure ceremony.
By codifying the most common patterns, the Rust team achieved two things. First, everyday code became noticeably shorter and easier to scan. Second, the places where you do see explicit lifetimes now carry real information: they signal a non‑trivial relationship between inputs and outputs. In a codebase that uses elision well, a lone 'a stands out as something worth understanding.
Origins:
The elision rules were introduced and stabilized early in Rust’s development, well before the 1.0 release. They have been part of the language since the first stable version and are documented in the Rust Reference and the Rustonomicon. The rules are not configurable — they are always active.
The Three Elision Rules
When the compiler encounters a function signature with missing lifetime annotations, it applies the following three rules in order. If the rules can assign a consistent lifetime to every reference position, the code compiles. If not, you get an error asking you to add annotations.
Rule 1 – Each input reference gets its own lifetime
For every reference that appears in the function’s parameters without an explicit lifetime, the compiler creates a fresh, distinct lifetime parameter. It does this even when the same type of reference appears multiple times.
Consider a function that prints two string slices and returns nothing:
fn show(x: &str, y: &str) {
println!("x: {x}, y: {y}");
}
The compiler sees two elided input lifetimes and internally expands the signature to:
fn show<'a, 'b>(x: &'a str, y: &'b str) {
println!("x: {x}, y: {y}");
}
Because there is no output reference, the compiler doesn’t need to decide whether the return value depends on 'a or 'b. Each input simply gets its own independent lifetime, and both are valid for the duration of the call. This is the least restrictive expansion possible — the two arguments are allowed to have different lifetimes, which is exactly what you want.
Rule 2 – One input lifetime propagates to all outputs
If the function has exactly one input lifetime (after Rule 1 has assigned them) and the return type contains elided references, the compiler assigns that single input lifetime to every output reference.
A classic example is returning a slice from a single string argument:
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}
After Rule 1, the input s gets a fresh lifetime, say 'a. Because there is exactly one input lifetime, Rule 2 applies and the output &str gets 'a. The compiler effectively treats the signature as:
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ }
This matches the real relationship: the returned slice is a sub‑borrow of s and therefore must not outlive s. Notice the function never had to name 'a — the pattern is so common that the compiler handles it silently.
You use this daily:
If you have ever written a method like fn trim(&self) -> &str and it compiled without lifetime annotations, you witnessed Rule 2 in action (combined with Rule 3 for methods). This covers a huge fraction of all borrowing functions.
Rule 3 – &self or &mut self supplies the output lifetime in methods
When a function takes &self or &mut self along with other references, the lifetime of the self reference is assigned to every elided output reference. This rule exists because methods that return borrowed data almost always tie that borrow to the receiver’s lifetime — the returned reference points into the struct or its owned data.
The following example shows a struct that holds a string slice, with a method that returns a sub‑slice of it:
struct Excerpt<'a> {
content: &'a str,
}
impl<'a> Excerpt<'a> {
fn snippet(&self, len: usize) -> &str {
&self.content[..len]
}
}
Even though the method signature has two references (&self and &str output), the compiler can apply Rule 3 because &self is present. It internally elaborates the method to:
fn snippet<'b>(&'b self, len: usize) -> &'b str {
&self.content[..len]
}
The output lifetime 'b is tied to &self, not to 'a directly (though 'b can’t outlive 'a because the struct holds &'a str). The key point is that the programmer doesn’t need to write either 'a or 'b on the method signature; the combination of the struct’s lifetime parameter and the elision rule keeps it clean.
Rule 3 only applies to the self parameter:
If a method has &self but you also explicitly annotate some other input reference with a lifetime, Rule 3 still uses self’s lifetime for the output. It does not choose the longer of the two, and it does not combine them. If you need the output to be tied to a different input, you must write the annotations yourself.
When all three rules fail — explicit annotation required
If a function has multiple input lifetimes and none of them are &self/&mut self, the compiler cannot decide which input lifetime to assign to the output. You’ll see the familiar error E0106: missing lifetime specifier.
The canonical example is a function that picks the longer of two string slices:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
Rule 1 creates two distinct input lifetimes, say 'a for x and 'b for y. Rule 2 doesn’t trigger because there is more than one. Rule 3 doesn’t apply because there is no self. The compiler stops and asks you to disambiguate. The fix ties both inputs and the output to the same lifetime:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
This is the only case where you must write lifetimes by hand in function signatures. Once you add them, the compiler can enforce the contract: the returned reference is valid for the intersection of x and y’s lifetimes.
This is the most common lifetime error beginners hit:
When you see error[E0106]: missing lifetime specifier on a function that returns a reference, the cause is almost always multiple input references without self. The fix is either to add explicit lifetimes or to redesign the function to return owned data (String instead of &str). Adding lifetimes is the correct answer only when the return value genuinely borrows from the inputs.
How the Compiler Applies the Rules in Practice
A useful mental model is to think of the compiler as walking through the function signature in a fixed order:
- Assign a distinct, anonymous lifetime to each input reference that doesn’t already have one.
- Count the remaining unassigned output reference positions.
- If there is exactly one input lifetime, assign it to every output.
- Otherwise, if one of the inputs is
&selfor&mut self, assign its lifetime to every output. - If neither condition holds, error.
This process is entirely mechanical. There is no “best guess” or complex reasoning — the rules are a simple decision tree. Understanding that tree lets you predict when you can drop annotations and when you must add them.
Consider a slightly more involved method:
struct Container<'c> {
data: &'c str,
}
impl<'c> Container<'c> {
fn search(&self, pattern: &str) -> Option<&str> {
self.data.find(pattern).map(|i| &self.data[i..])
}
}
Applying the tree:
&self→ gets its own lifetime, call it's.pattern: &str→ gets its own lifetime, call it'p.- Output
Option<&str>is unassigned. - There are multiple inputs, but one is
&self, so Rule 3 fires: output gets's.
The expanded signature is:
fn search<'s, 'p>(&'s self, pattern: &'p str) -> Option<&'s str>
The pattern can be arbitrarily short‑lived, but the returned reference is tied to self’s borrow. The code compiles without any lifetime annotations because the rules matched the intent.
Common Misconceptions
“Lifetime elision means lifetimes disappear.” They don’t. The compiler still tracks every reference exactly as if you had written the annotations. Elision only controls what appears in source code; the borrow checker’s behaviour is unchanged.
“Elided lifetimes are always 'static.” They are not. Elided lifetimes are inferred from the function’s inputs or self. The only way a reference gets the 'static lifetime is if it comes from a static item, a string literal, or you explicitly write 'static.
“I can use elision anywhere, even in struct definitions.” Struct fields that hold references always require explicit lifetime parameters. Elision rules operate on function signatures (and function‑like constructs such as trait methods and closures), not on type definitions.
“If my function compiles with elision, the output lifetime is exactly the input lifetime.” Not necessarily — it can be a sub‑lifetime. The returned reference might point to a sub‑slice of the input, meaning it borrows for a shorter duration. The key property is that the output lifetime is bounded by the input lifetime, not equal to it.
Beware of combining multiple inputs with Rule 3:
A method like fn combine(&self, other: &str) -> &str will compile via Rule 3, but the output is tied only to &self, not to other. If your logic returns a sub‑string of other, the code will compile yet produce a reference whose true source is other — this can lead to a later borrow‑checker error at the call site when self outlives other. Always verify that the elided output really borrows from self; if it doesn’t, you need explicit annotations.
Real‑World Usage and Intent
Elision is not just about typing less — it also communicates intent. When you read a signature like fn trim(&self) -> &str, the absence of named lifetimes tells you immediately that the return value is a straightforward borrow from self. There is no complex lifetime algebra to decode.
Explicit lifetimes, by contrast, draw attention to a non‑trivial relationship. A signature like fn reconcile<'a, 'b>(left: &'a str, right: &'b str) -> &'a str signals that the return value borrows exclusively from left. Readers know they cannot ignore that 'a.
Experienced Rust developers let elision handle the routine cases and reserve manual annotations for places where the borrowing structure is genuinely ambiguous. This keeps the codebase readable while maintaining full control over safety where it matters.
When you do need to write lifetimes, choose names that convey meaning. Instead of 'a, 'b, 'c, consider 'input, 'output, or 'src. In a method that processes a buffer, fn process<'buf>(&'buf self) -> &'buf str instantly communicates that the return value borrows from the internal buffer. This is a small habit that pays off as codebases grow.
Summary
Lifetime elision is the compiler’s way of filling in the blanks when you omit lifetime annotations from function signatures. It relies on three rules that mirror the most common borrowing patterns: each input starts with its own lifetime; a single input lifetime propagates to all outputs; and &self/&mut self supplies the output lifetime in methods. Any situation that falls outside these patterns requires explicit annotations.
The rules exist so you can write safe code without ceremony while still benefiting from Rust’s zero‑cost memory safety guarantees. Once you internalise them, you will rarely need to write lifetime annotations at all — and when you do, you’ll know exactly why they are necessary.