RFA-532 · Case file with fixtures · Case 504 of 694 · Compiler evidence
Rust Lifetime Elision Can Hide a Missing Generic Outlives Bound
Elision creates real unnamed lifetime parameters. When a generic helper relates T to that lifetime, the caller may need to name and expose the same relation.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Elision creates a real caller-chosen lifetime, but nothing in the outer inputs implies that unrelated generic T remains valid for it.
- First discriminating check
- Expand the elided signature, find which helper introduces T: 'a, and either expose that named relationship or remove an unnecessary downstream bound.
Lifetime elision makes ordinary Rust pleasant, but it can hide the exact relationship a generic call needs. E0311 is one place where I expand the hidden lifetime on paper before changing any code.
The failing fixture declares preserve<T>(token: &()) -> &(). It calls a helper whose signature requires T: 'a for the token's lifetime. The outer function has no such promise.
Elided does not mean absent
Rust applies lifetime-elision rules to create a lifetime for the input reference and connect the returned reference to it. We can imagine the signature as having some caller-chosen 'anon:
fn preserve<'anon, T>(token: &'anon ()) -> &'anon ()
The exact generated name is irrelevant. What matters is that this is a real lifetime, and the function must work for every legal choice a caller makes.
The lifetime-elision reference specifies when these input and output relationships are inferred. Elision removes notation; it does not weaken checking.
The helper introduces a relationship with T
preserve_with_bound<'a, T: 'a> accepts the same token reference only when T outlives 'a. The outer function is generic over any T, including a type that may contain a reference shorter than the token borrow.
The outer body cannot manufacture T: 'anon. Its public signature promised more calls than its implementation can support, so Rust rejects the definition.
The official E0311 explanation describes this as an unsatisfied outlives bound involving an elided region and a generic parameter or associated type.
The repair names what the implementation already requires
The repaired fixture writes:
fn preserve<'a, T: 'a>(token: &'a ()) -> &'a ()
Now the input lifetime, output lifetime, and generic outlives requirement share one visible name. Callers see the true contract, and the helper call is justified.
I do not consider the explicit lifetime to be noise here. Once a lifetime participates in more than the usual input-to-output relationship, giving it a name makes the extra obligation readable.
T outlives a is about possible contents of T
T: 'a does not keep a particular T value alive. It requires that borrowed data inside T be valid for 'a. Owned types such as String generally satisfy many such bounds. A type containing &'short str can satisfy it only when 'short outlives 'a.
This is why the compiler cannot infer the promise simply because no T value appears in the body. Monomorphisation does not excuse a generic function from being valid for its whole declared domain.
The Reference's lifetime bounds section is my source when “outlive” starts sounding like runtime object retention.
Using a reference to T can create an implied bound
If the function accepted &'a T, well-formedness of that reference would imply T: 'a. In this fixture it accepts &'a (), so there is no relationship between T and the reference from which Rust could derive the bound.
I do not add an unused &T merely to obtain an implied bound. That would distort the API. I either state the required T: 'a contract or redesign the helper so it does not require the unrelated relationship.
Sometimes the better repair is to remove the bound downstream
A helper may carry an old or overly strong lifetime constraint. Before propagating it through ten call layers, I ask whether its implementation truly stores or projects anything involving T for 'a.
If not, removing the unnecessary helper bound gives a more general API. If yes, surfacing the bound in the caller is honest. Compiler-driven design is strongest when I inspect the semantic origin, not only make the red message disappear.
E0311 and E0309 are close but not identical
Both diagnostics involve a parameter that may not live long enough. E0309 often appears while checking a type definition or associated-type projection missing an explicit bound. E0311 specifically highlights an unsatisfied relationship involving an elided region.
The practical debugging move is the same at first: expand hidden lifetimes. After that, I follow the concrete obligation chain rather than treating all lifetime messages as one category.
My E0311 checklist
- Which reference lifetime was elided in the public signature?
- What explicit signature do the elision rules imply?
- Which called function requires a generic parameter to outlive it?
- Is there an input such as
&'a Tthat already implies the bound? - Should I name
'aand stateT: 'aat this boundary? - Is the downstream bound genuinely necessary?
- Am I reaching for
'staticwhen a local named lifetime is enough? - Does the final signature explain the data relationship to a reader?
The core principle is that elision hides spelling, not obligations. When a generic type becomes related to an elided borrow, I name that borrow and make the true outlives contract explicit.