When the compiler guesses wrong
You write a method on a struct. It looks simple enough. You want it to hand back a reference to one of the struct's fields. You compile it, and the compiler throws a lifetime error. You stare at the screen. The code looks fine. The reference should live as long as the struct does. The compiler disagrees. This happens because Rust treats self like any other parameter, and it applies the same lifetime rules to it. The compiler's shortcuts work most of the time, but they break down when references flow in and out of methods in unexpected ways.
How self and lifetimes actually interact
Methods in Rust are just functions with a special first parameter called self. When you write fn do_something(&self), you are actually writing fn do_something(self: &Self). The & means you are borrowing the instance. You are not taking ownership. You promise to give it back when the method finishes. Lifetimes track how long that borrow lasts.
Rust saves you from writing lifetime annotations everywhere through lifetime elision. The compiler inserts them automatically based on three simple rules. The first rule covers almost all method signatures. If the method has a &self or &mut self parameter, the lifetime of that borrow is assigned to all output references. The compiler assumes the returned reference lives exactly as long as the instance you called the method on. This keeps your code clean. It also hides the mechanics until you need to see them.
Convention aside: the Rust community writes &self instead of self: &Self because it is shorter and instantly recognizable. cargo fmt will not change it either way, but every idiomatic codebase uses the shorthand. Stick with it.
Trust the elision rules for single-reference inputs. They handle ninety percent of method signatures correctly.
The minimal working case
struct Document {
title: String,
content: String,
}
impl Document {
/// Returns a reference to the title field.
fn get_title(&self) -> &str {
// The compiler infers the lifetime here.
// It matches the borrow of self.
&self.title
}
/// Updates the content and returns a reference to the new text.
fn set_content(&mut self, new_text: &str) -> &str {
// We take ownership of the string to avoid borrowing issues.
self.content = new_text.to_string();
// The returned reference is tied to &mut self.
&self.content
}
}
Walking through the compiler's expansion
Look at get_title. You wrote &self and &str. The compiler expands this behind the scenes to fn get_title<'a>(&'a self) -> &'a str. The 'a ties the input borrow to the output reference. If the Document lives for ten seconds, the returned &str can also live for ten seconds. The compiler guarantees you cannot return a reference to something that dies inside the method.
Now look at set_content. It takes &mut self and a &str parameter. The elision rule still applies. The output &str gets the lifetime of &mut self. The new_text parameter gets its own independent lifetime. The compiler knows they are separate. It does not force them to match. This separation prevents accidental aliasing. You can pass a temporary string, mutate the struct, and return a reference to the struct's internal data without confusing the borrow checker.
Convention aside: when you need to name lifetimes explicitly in methods, 'a is the standard default. It is short, unambiguous in simple signatures, and matches the standard library. Only switch to descriptive names like 'input or 'ctx when a single method contains three or more distinct lifetimes.
Write the signature the compiler expects. Let elision do the heavy lifting.
When elision hits a wall
Elision breaks when a method takes multiple reference parameters and returns a reference. The compiler does not guess which input lifetime belongs to the output. It stops and asks you to specify.
struct Parser {
delimiter: char,
}
impl Parser {
/// Returns the longer of two input strings.
/// Elision fails here because there are two input references.
fn pick_longer<'a>(&self, first: &'a str, second: &'a str) -> &'a str {
// Both inputs share the same lifetime annotation.
// The compiler now knows the output cannot outlive either.
if first.len() >= second.len() {
first
} else {
second
}
}
}
The compiler sees two input lifetimes. It cannot assume they are the same. It also cannot assume the output ties to self. You must declare the relationship. By writing <'a> and applying it to both parameters and the return type, you tell the compiler that the returned reference will live as long as the shorter of the two inputs. The method signature becomes a precise contract. The borrow checker enforces it at compile time.
Convention aside: you will see &self without a lifetime annotation in the same signature. That is correct. The compiler assigns self its own independent lifetime automatically. It does not need to share 'a unless the method actually returns a reference tied to self.
Declare the relationship explicitly. The compiler will not guess for you.
Pitfalls and compiler rejections
The most common mistake is returning a reference to a local variable. The compiler catches this with E0515 (returned value does not live long enough). You create a String inside the method, take a &str slice of it, and try to return it. The String drops at the end of the function. The slice points to freed memory. Rust refuses to compile it. The fix is to return an owned String or a reference to data that lives outside the method.
Another trap is mixing self by value with reference returns. If you write fn consume(self) -> &str, the compiler rejects it. You moved the struct into the method. The method owns it now. When the method returns, the struct is dropped. There is nothing left to reference. The compiler throws E0515 again. If you need to return a reference, you must borrow the instance with &self or &mut self.
Lifetime elision also causes confusion when you accidentally return a reference tied to a short-lived argument instead of self. The compiler will complain about mismatched lifetimes. You will see an error about expected &'a str but found &'b str. The solution is to align the lifetime annotations with the actual data flow. If the output depends on self, tie it to self. If it depends on an argument, tie it to that argument.
Convention aside: when the compiler rejects a signature with E0106 (missing lifetime specifier), do not panic. The error message usually shows the expanded signature it expects. Copy the lifetime annotations it suggests. Verify they match your data flow. Adjust if necessary.
Treat lifetime annotations as contracts. Write them down when the compiler cannot infer the relationship between inputs and outputs.
Choosing the right signature
Use &self when the method only reads data and does not modify the struct. Use &mut self when the method changes internal state or needs exclusive access to a field. Use self by value when the method consumes the instance entirely, such as converting it into another type or extracting owned data. Rely on lifetime elision when the method has exactly one input reference or when &self is the only reference parameter. Write explicit lifetimes when the method takes multiple reference parameters and returns a reference, forcing you to declare which input lifetime the output depends on. Skip unsafe blocks entirely for lifetime management, because the borrow checker handles this at compile time without runtime overhead.
Pick the signature that matches the data flow. The borrow checker will enforce the rest.