You write a one-line function. It has no lifetime annotations in it anywhere. The build fails with a message about lifetimes.
pub fn items(scores: &[u8]) -> std::slice::Iter<u8> {
scores.iter()
}
warning: hiding a lifetime that's elided elsewhere is confusing
= help: use `'_` for type paths
Warn-by-default since Rust 1.89 — and since this site grades with
-D warnings, a hard error. If nobody has briefed you, this is maximally
confusing. Here is the briefing.
Three ways to spell one lifetime
Every lifetime in a signature is written in one of three groups:
| group | how it looks | examples |
|---|---|---|
| named | you wrote a name |
&'a str, Person<'a>, Iter<'a, u8> |
| elided | you wrote a placeholder |
&str, &'_ str, Person<'_>, Iter<'_, u8> |
| hidden | nothing visible at all |
Person, Iter<u8> |
The first two are familiar. The third is the one that catches people.
std::slice::Iter is declared Iter<'a, T>. It has a lifetime parameter. But
Rust lets you write a path with its lifetime arguments entirely omitted, so
Iter<u8> is legal and means Iter<'_, u8> — a lifetime with no visual
indication whatsoever that it exists. Same for a bare Person when the type
is Person<'a>.
The rule
mismatched_lifetime_syntaxes fires when one lifetime is referred to using
syntaxes from different groups within a single signature.
That is the whole rule. It is about grouping, not spelling.
pub fn items(scores: &[u8]) -> std::slice::Iter<u8> // elided input, hidden output -> fires
pub fn items(scores: &[u8]) -> std::slice::Iter<'_, u8> // both elided -> quiet
pub fn items<'a>(scores: &'a [u8]) -> std::slice::Iter<'a, u8> // both named -> quiet
All three of these compile and mean exactly the same thing. Only the first is rejected, and it is rejected for being confusing to read, not for being wrong.
The lint has three wordings depending on which groups collided, and they are worth recognising:
hiding a lifetime that's elided elsewhere is confusing // Iter<u8> vs &[u8]
hiding a lifetime that's named elsewhere is confusing // V vs &'a str
eliding a lifetime that's named elsewhere is confusing // &u8 vs &'a u8
And crucially, what does not fire:
pub fn f(x: &u8) -> &'_ u8 { x } // quiet
&u8 and &'_ u8 are different spellings within the same group. The lint
does not care. Anyone who tells you '_ is mandatory has mis-stated the rule.
💡Which of these four fire the lint? Say which two groups collide in each one that does. click to reveal
pub struct V<'a> { s: &'a str }
pub fn a<'x>(v: &'x V<'x>) -> &str;
pub fn b(v: V) -> V;
pub fn c(s: &str) -> std::str::Chars<'_>;
pub fn d<'x>(s: &'x str) -> std::str::Chars<'x>;
a fires. 'x is named in the parameters and elided in the return (&str). Named vs elided → “eliding a lifetime that’s named elsewhere is confusing”. Note this signature is otherwise fine — elision even supplies the output lifetime, since &'x V<'x> is one parameter whose positions agree. It still fails the gate.
b is quiet. Both V occurrences are hidden. One group, no collision. This is legal, passes the gate, and is arguably the least readable line here — which tells you the lint is a consistency check, not a readability oracle.
c is quiet. &str and Chars<'_> are both elided.
d is quiet. Both named.
The takeaway from b: the lint will not stop you writing something opaque. It only stops you from writing something inconsistent. Choosing to be clear is still your job.
'_ does not mean “no lifetime”
This is the misreading that turns the fix into a mystery. '_ means:
“There is a lifetime here. Infer it.”
It is an explicit placeholder — a visible marker that the type is
lifetime-parameterised, with the actual value left to inference. That is why it
is the fix rather than a way to opt out: it moves the lifetime from hidden to
elided, matching the &[u8] parameter, and the inconsistency is gone.
Two names you will see in older material
elided_named_lifetimes was the older, narrower lint that
mismatched_lifetime_syntaxes replaced. Posts and issues written before 1.89
use that name; the lint no longer exists under it.
elided_lifetimes_in_paths is the lint that would ban hidden lifetimes
outright — it fires on Iter<u8> and Person regardless of consistency. It is
still allow-by-default, and the compiler’s own description says “hidden
lifetime parameters in types are deprecated”. Some codebases turn it on as a
house style; the default toolchain does not, and it is not part of this site’s
gate.
So: '_ in paths is not mandatory in general. Consistency within a
signature is.
💡This fails the gate. Apply the mechanical fix — then say what the fix just revealed, and why that is the real value of the lint. click to reveal
pub struct Config<'a> { root: &'a str }
pub fn load(text: &str) -> Config { .. }
The mechanical fix: &str is elided, Config is hidden, so this is “hiding a lifetime that’s elided elsewhere is confusing”. Write -> Config<'_> and it passes.
Now the interesting part. Before the fix, the signature said Config, which reads like an owned, self-contained value. After the fix it says Config<'_>, which announces that the returned config is a view into text — the caller must keep text alive for as long as they hold the config.
That is a significant fact about the API, and it was completely invisible. For a config object, which typically gets stored and consulted much later, borrowing is often the wrong shape, and Config { root: String } would serve callers better. The lint did not tell you that. It made the question askable by putting the lifetime on the page.
This is exactly why hiding lifetimes is discouraged: a reader who cannot see the parameter cannot notice the design decision, let alone challenge it.
The practical checklist
When this lint fires:
- Find the lifetime it is complaining about. The message names both spans.
-
Pick one group and use it in both places. Usually that means writing
'_in the path. -
Get the slot right.
SplitisSplit<'a, P>— two parameters, lifetime first.Split<'_, char>is correct;Split<char, '_>is a syntax error andSplit<'_>is missing a type argument. - If the type has several parameters and you are unsure, hover it or check the docs. Guessing produces a confusing second error.
And a note on the alternative you will sometimes prefer: returning
impl Iterator<Item = &u8> + '_ instead of a concrete Iter<'_, u8> also
satisfies the lint, hides the concrete type from your callers, and gives you
freedom to change the implementation later. Different tradeoff, equally
legitimate.