Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-494 · Case file with fixtures · Case 466 of 694 · Compiler evidence

Rust Trait Method Lifetime Bounds Must Match the Declaration

Lifetime bounds are part of a trait method's quantification. An implementation must preserve the declaration's relationships rather than relying on a body that happens to work for some calls.

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
Lifetime parameters quantify the borrow relationships admitted at each call, so removing or strengthening an outlives bound changes the method contract.
First discriminating check
Expand elided lifetimes, write each outlives relationship in plain language, and make the impl's quantified bounds equivalent to the trait declaration.

I declared a trait method with 'long: 'short, then omitted that relationship in the implementation. Rust emitted E0195 because method lifetime bounds are part of the trait contract, even when the parameter and return types look visually close.

The failing fixture returns the longer-lived input as a reference valid for the shorter lifetime. That conversion depends on the declared outlives relationship. The impl cannot silently use a different set of permitted calls.

Read an outlives bound in plain language

The Rust Reference on lifetime bounds defines 'long: 'short as the first lifetime lasting at least as long as the second. A reference valid for 'long may therefore be used where 'short is required.

The names are only labels. 'a: 'b has the same structural meaning. Renaming lifetime parameters in an impl can be fine; dropping or changing their relationships is not.

I usually rewrite the bound as a sentence before editing code: “the selected value must remain valid for the requested output period.”

A trait method is quantified over its declared lifetimes

Generic lifetimes do not mean one pair chosen forever. The caller supplies lifetimes satisfying the contract at each call. The implementation must work for that admitted set.

If the impl removes a bound, adds a stronger one, or places it in a non-equivalent scope, it describes a different family of calls. Rust compares this family rather than accepting a body that type-checks in isolation.

The official E0195 page advises matching both parameters and bounds exactly.

The repaired fixture restores the relationship

The repaired fixture repeats 'long: 'short on the impl method. Its body can then return long as 'short safely, and the call verifies the result.

In production code, I may express the same requirement in a where clause for readability. I still check that its scope and meaning match the trait. Moving tokens around is safe only when the quantified contract remains equivalent.

Copying the declaration first and then renaming values is often the fastest reliable repair.

Elision can hide the important difference

Simple reference signatures let Rust infer lifetimes through elision rules. Once several inputs and an output interact, an explicit relationship may appear in the trait while the impl relies on an incorrect intuition about inference.

I expand the lifetimes on paper or in a reduced fixture. I ask which input the output borrows from and how long each reference must remain usable. This usually reveals whether the desired implementation actually matches the public promise.

Adding 'static is almost never a general repair; it often rejects useful borrowed data and hides the intended relationship.

Async and macro transformations make this error harder

An async trait helper or procedural macro may transform method lifetimes into generated future types and where clauses. Version drift between the trait macro and impl macro can produce E0195 far from the source-level intent.

I inspect expanded code when possible and verify the exact macro and dependency versions. I also reduce the contract to a synchronous example if that makes the quantification easier to see.

The goal is to understand the relationship, not to paste lifetime parameters until the diagnostic moves.

Public trait evolution needs caution

Changing a lifetime bound may widen or narrow which callers are valid. It can also invalidate every downstream implementation even if no method name changes.

When I own the trait, I treat this as an API change and compile representative external impls. When I do not own it, I follow the resolved declaration exactly and put any additional constraints in the surrounding concrete code instead of strengthening the impl method.

My E0195 checklist

  • How many lifetime parameters does the trait method introduce?
  • Which lifetime must outlive which other lifetime?
  • Does the impl preserve the same quantification and bounds?
  • Is a where-clause form truly equivalent?
  • Which input does the returned reference borrow from?
  • Did elision hide a relationship I need to write explicitly?
  • Are macros or dependency versions generating different signatures?
  • Would adding 'static unnecessarily reject borrowed callers?

The core principle is that lifetimes describe accepted relationships between borrows. A trait implementation must honour the same relationships as the declaration, not merely produce a body that works for one convenient example.