RFA-523 · Case file with fixtures · Case 495 of 694 · Compiler evidence
Rust Trait Objects with Multiple Derived Lifetimes Need an Explicit Bound
When supertraits imply several lifetime bounds, trait-object defaulting may have no unique answer. Name one object lifetime and state that it outlives every required bound.
- 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
- The erased implementor must satisfy several supertrait lifetime obligations, but default trait-object lifetime rules cannot select one bound from independent candidates.
- First discriminating check
- Introduce one explicit object lifetime, map every supertrait relationship, and verify the object lifetime outlives each required lifetime with non-static scope tests.
I built a trait object from Transfer<'source, 'target>, whose supertraits require both lifetimes. Rust emitted E0227 because it could not derive one unique object lifetime bound.
The failing fixture uses Box<dyn Transfer<...>>. The box is owned, but the erased implementor may still contain references and must satisfy an object lifetime.
Trait objects need one coherent object lifetime
An object lifetime bound describes how long references captured inside the erased implementor must remain valid. It is written like dyn Trait + 'object.
Defaulting rules often infer a unique bound from context or trait requirements. Here two different supertrait relationships supply candidates, so no one answer is forced.
The official E0227 page asks for an explicit lifetime.
The repaired fixture names the missing relationship
The repaired fixture introduces 'object, writes dyn Transfer<...> + 'object, and states 'object: 'source + 'target.
That direction matters. Because the trait object must meet both supertrait lifetime requirements, its object lifetime outlives each required lifetime in this declaration.
The first attempted reverse bounds failed with E0478, and the evidence was corrected before publication.
Box ownership does not imply static contents
Box<dyn Trait> owns the erased value, but that value can itself borrow data. Heap allocation changes where the value is stored, not how long its internal references remain valid.
Defaulting to 'static is common in some type contexts when no other bound is available, but adding it explicitly can reject valid short-lived implementations. I write 'static only when the object must truly contain no shorter borrow.
Ownership and borrowed content are separate axes.
Supertrait bounds accumulate constraints
Transfer<'s, 't>: ReadFrom<'s> + WriteTo<'t> means an implementor must satisfy both capabilities. If each supertrait also requires its own lifetime, the final object carries both obligations.
Same spelling or conceptual relation does not merge independent lifetimes. I map the supertrait graph and write each outlives statement as a sentence before choosing the object bound.
This prevents reversing a colon under pressure.
References around objects add another lifetime
&'borrow (dyn Trait + 'object) has an outer reference lifetime and an inner object lifetime. The reference says how long the handle is borrowed; the object bound constrains references inside the implementor.
They may be equal in a simple API but do not mean the same thing. Parentheses make the nested structure readable when additional + bounds exist.
The default object lifetime Reference documents this separate inference system.
Simpler ownership may remove the ambiguity
Sometimes the traits can own their source and target state instead of borrowing it, removing lifetime parameters from the object contract. In other cases one request-scoped lifetime can replace two independently named ones.
I do not collapse real independent borrows only to simplify syntax. But E0227 is a useful prompt to check whether the public trait graph carries more lifetime dimensions than users need.
Test with genuinely different lifetime scopes
If every test uses string literals, all inputs may be 'static and an incorrect relationship remains hidden. I construct owners in nested scopes and ensure the object cannot escape the shortest permitted borrow. Compile-fail examples are especially useful because the desired rejection is part of the contract.
I also test the positive direction with two owners whose scopes differ. This catches reversed bounds like my first repair attempt. Lifetime parameter names such as 'long and 'short help humans, but only scope and outlives constraints give them meaning. The executable fixture, not the name, proves the direction.
Consider separating capabilities
When one object simultaneously reads borrowed source state and writes borrowed target state, it may be doing too much. Separating a plan from one execution can let the plan own configuration and let a short-lived method borrow source and target, removing lifetimes from the stored object. This is an architectural option, not a universal fix, but it often simplifies long-lived registries.
My E0227 checklist
- Which trait and supertrait bounds imply object lifetimes?
- Is there exactly one derived candidate or several?
- What does the chosen
'objectlifetime describe? - Which direction must every outlives relation point?
- Does the erased implementor borrow data despite Box ownership?
- Are outer reference and inner object lifetimes distinct?
- Would
'staticbe an unnecessary restriction? - Can the trait graph own data or share one honest lifetime instead?
The core principle is that erased types still carry borrow obligations. When several lifetimes reach one trait object, I state the single object bound and its relationships explicitly.