Mehdi Akiki
Rust Failure Atlas / Runtime, memory, and library APIs

RFA-531 · Case file with fixtures · Case 503 of 694 · Compiler evidence

Projected Associated Types Can Require an Explicit Outlives Bound

An associated-type projection imports the bounds needed to select its implementation. The containing type must state those lifetime obligations at its own boundary.

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
Associated-type projection imports the selected implementation's well-formedness bounds, while the container currently admits shorter borrowed types.
First discriminating check
Inspect the projected trait implementation, translate its outlives requirement, and state the smallest honest bound on the container that fundamentally needs it.

E0309 became clearer for me when I stopped reading T: 'a as “the value T lives for 'a.” A type is not a running value. The bound says that references carried inside any admitted T must remain valid for at least 'a.

The failing fixture stores <T as Project<'a>>::Output. The blanket Project implementation exists only when T: 'a, but Snapshot<'a, T> admits every T. Its field therefore asks for a projection that is not defined for all types allowed by the struct header.

A projection is a type-level lookup with conditions

<T as Project<'a>>::Output looks like a finished type. In reality, Rust must select a matching trait implementation and then read its associated type. Bounds on that implementation are part of the selection.

Our implementation says:

impl<'a, T: 'a> Project<'a> for T {
    type Output = &'a T;
}

The output itself makes the need concrete: a reference &'a T cannot be well formed if T may contain a shorter reference. The compiler is checking the definition for every legal substitution, not only the friendly value I hope to construct in main.

The official E0309 page uses this associated-type shape because it exposes exactly where the missing obligation comes from.

The container must advertise the obligation

The repaired fixture adds where T: 'a to Snapshot. Now callers know that constructing this type requires a T compatible with 'a, and the associated-type projection has enough evidence.

This is not a local compiler hint. It narrows the public set of valid Snapshot instantiations. If the struct is public, the bound is part of its API contract and deserves the same care as a field type.

I place the bound on the earliest abstraction that fundamentally needs it. Repeating it only inside unrelated methods can leave the type definition invalid or force callers to rediscover the same rule in many places.

Outlives bounds inspect references contained by a type

For a plain owned String, String: 'a is normally easy to satisfy because it carries no borrowed data. For &'short str, the same bound requires 'short: 'a. This is the part that matters in real generic code.

If T is a struct containing several references, every relevant borrowed component must be valid long enough. Ownership often removes the constraint, while nesting it can propagate the constraint through several layers.

The Reference defines these relationships under lifetime bounds. I translate each bound into a statement about references before changing it.

Do not answer every lifetime error with static

Adding T: 'static might also satisfy many shorter requirements, but it states a much stronger contract. It excludes types containing ordinary local borrows even when those borrows would live long enough for the actual snapshot.

I reserve 'static for data that genuinely carries no non-static references or for interfaces that may retain values without a caller-bounded lifetime. Most request, parsing, and view objects need a named local relationship instead.

The smallest correct statement here is T: 'a.

Associated types can hide the origin of the error

In a large codebase, the projection may come from a dependency and the required bound may be several definitions away. I inspect three places:

  1. the container whose field fails;
  2. the trait declaring the associated type;
  3. the implementation selected for the projected type.

The associated types section helps separate the trait declaration from the implementation that chooses a concrete output.

Compiler notes often point to the missing T: 'a requirement. I still verify why that implementation needs it rather than pasting the suggestion without understanding the API impact.

A type annotation may still be needed at construction

The repaired fixture writes Snapshot<'_, String> for the local value. This solves a separate inference question. Because the compiler sees only &String as the projected field value, several hypothetical implementations could map different inputs to that same output. Naming T selects the intended projection.

E0309 concerns well-formed lifetime obligations. E0284 can concern selecting one unique type. A repair can expose the next independent question, and that does not mean the first repair was wrong.

My E0309 checklist

  • Which parameter does the compiler say may not live long enough?
  • Does a field contain an associated-type projection?
  • Which implementation is being selected for that projection?
  • What outlives bound does that implementation require?
  • Does T: 'a constrain references stored inside T as intended?
  • Should the containing type expose the bound, or can its representation change?
  • Am I accidentally strengthening a local lifetime to 'static?
  • After lifetime repair, is any separate type inference still ambiguous?

The core principle is that associated types do not erase their implementation requirements. When a container stores a projection, I make the needed lifetime relationship visible at the container boundary.