RFA-488 · Case file with fixtures · Case 460 of 694 · Compiler evidence
A Rust Trait Object Allows Only One Explicit Lifetime Bound
A trait object has one object-lifetime bound describing how long captured references inside the erased implementor remain valid. Choose one lifetime that satisfies the required relationships instead of listing alternatives.
- 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 concrete contents need one coherent outlives requirement; multiple source lifetimes must relate to one chosen object lifetime outside the dyn bound.
- First discriminating check
- Identify references hidden in the implementor, introduce one object lifetime, and state how each source outlives it rather than listing two object bounds.
I defined type Reader<'a, 'b> = dyn Read + 'a + 'b. Rust emitted E0226 because a trait object permits only one explicit lifetime bound.
The failing fixture exposes a misconception: the two bounds do not mean “valid for either lifetime” or automatically compute their intersection. One erased object type needs one lifetime requirement.
The bound describes references hidden in the implementor
dyn Read + 'a means the concrete erased value must outlive 'a; any references it contains must remain valid for at least that lifetime.
The trait-object Reference describes trait objects as a base trait plus auto traits and at most one lifetime bound.
This bound belongs to the object type. A surrounding &'x reference to the object has another, related lifetime that describes the borrow of the object itself.
Choose one lifetime that expresses the storage contract
The repaired fixture keeps only 'a:
type Reader<'a> = dyn Read + 'a;
In real code, I choose the lifetime from where the object will be stored and which borrowed data the concrete implementation may contain. Removing one at random can over-constrain or under-describe the intended relationship.
The shortest relevant lifetime is often expressed through a fresh parameter with explicit outlives bounds.
Relate input lifetimes to one object lifetime
If an erased adapter may borrow from two sources, I can introduce 'obj and require both source relationships appropriate to construction, for example that each source outlives 'obj.
Then the object type carries only + 'obj. The where clause explains how 'obj relates to 'a and 'b rather than placing both directly on dyn Read.
The exact direction of outlives bounds matters, so I derive it from the stored references and verify with a reduced fixture.
Defaults depend on context
Rust has default trait-object lifetime rules that differ from ordinary reference lifetime elision. The default object lifetime Reference lists how containing types, trait bounds, and expression context select a default.
Box<dyn Read> often implies 'static where a borrowed implementation was expected. Writing Box<dyn Read + 'a> makes borrowed storage explicit.
I avoid relying on a default when the object crosses an API boundary and lifetime intent is important.
static is not a performance or ownership marker
dyn Read + 'static does not require the object itself to live forever. It requires the erased concrete value to contain no non-static borrowed data.
An owned String-backed reader can satisfy 'static and still be dropped immediately. A reader borrowing a local slice usually cannot.
This distinction prevents unnecessary leaks and clones in attempts to satisfy trait-object errors.
Generic dispatch may avoid erasure
If every caller handles one reader type at a time, R: Read + 'a can preserve the concrete type and make relationships easier to express. Trait objects are useful for heterogeneous runtime selection, stable indirection, or reduced code exposure.
I do not switch to generics only to escape lifetime reasoning; both forms still require borrowed data to remain valid. The API's polymorphism needs guide the choice.
My E0226 checklist
I test the chosen lifetime with one owned implementation and one borrowing implementation. The owned case confirms I have not made ordinary use unnecessarily difficult; the borrowed case confirms the object does not default to 'static or outlive its source. A compile-fail test for returning the object after its backing buffer drops can be valuable in a library. These examples document the actual storage promise more clearly than a lifetime letter alone.
If an API needs several independent borrows, a named context struct may make the shared object lifetime easier to derive and explain.
- What references may the erased concrete type contain?
- How long must the stored object remain valid?
- Can one fresh object lifetime relate to all source lifetimes?
- What outlives direction do those relationships require?
- Is a default object lifetime silently choosing
'static? - Am I confusing object validity with the borrow of its pointer?
- Would owned data satisfy the contract without leaking?
- Is runtime type erasure actually required?
The core principle is that a trait object has one coherent lifetime contract for its erased contents. I express relationships to multiple inputs outside the object bound and keep one explicit lifetime on the object itself.