RFA-127 · Case file with fixtures · Case 99 of 694 · Compiler evidence
Why Box<dyn Trait> Defaults to 'static for a Borrowed Value
An omitted trait-object lifetime is not always inferred from nearby input lifetimes. In Box<dyn Trait>, default object lifetime rules commonly select 'static; write dyn Trait + 'a when the object may contain an 'a borrow.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all targets
- Profiles
- check, dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Outside expressions, an omitted trait-object lifetime follows default object lifetime rules, and Box<dyn Trait> selects 'static when no containing lifetime supplies another bound.
- First discriminating check
- Expand the return type mentally from Box<dyn Trait> to Box<dyn Trait + 'static>, then attach the borrow lifetime explicitly if ownership is not intended.
The failing program creates a small Borrowed<'a> value and returns it behind Box<dyn Label>. The function input clearly has lifetime 'a, yet rustc says that 'a must outlive 'static.
This error is confusing when I expect ordinary function lifetime elision to connect the input borrow and the returned object. The missing detail is that trait objects have their own default object lifetime rules.
The hidden lifetime inside dyn Trait
A trait object can hide a concrete type that contains references. Rust therefore needs a bound describing how long those hidden references remain valid:
dyn Label + 'a
When the bound is omitted, the compiler follows the default trait-object lifetime rules. These rules are separate from the usual lifetime-elision rules used in function signatures.
For a boxed object in this return position, Box<dyn Label> becomes, in effect:
Box<dyn Label + 'static>
'static does not mean the box itself lives forever. It means the concrete value stored behind the trait object cannot contain a reference shorter than 'static. Borrowed<'a> contains the caller's &'a str, so it does not satisfy that contract.
The repair states the real object lifetime
The repaired program returns this instead:
fn boxed<'a>(text: &'a str) -> Box<dyn Label + 'a>
Now the box owns the Borrowed structure, while that structure still borrows text for 'a. Ownership of the outer allocation and lifetime of the inner data are two different facts.
This is one reason I avoid saying “the box owns everything.” The Box documentation describes owned heap allocation, but a boxed value can itself contain borrowed references. Moving the box does not extend those references.
The return value cannot outlive the source string, and Rust makes that limitation visible to callers. That is the desired safety contract.
A reference to a trait object is a different shape
These types express different ownership:
&'a dyn Label
Box<dyn Label + 'a>
Box<dyn Label + 'static>
The first borrows an already existing implementer. The second owns the concrete implementer but permits it to borrow other data for 'a. The third owns an implementer containing no non-static borrows.
I choose among them based on who should destroy the implementer and whether it needs to share another value's lifetime. I do not choose Box only to make a type error disappear.
Owning the text is another valid repair
Sometimes the returned object should be independent of the input. Then I change the hidden implementation to own a String or another owned representation. A Box<dyn Label> with its default 'static bound can be correct because no borrowed input remains inside it.
This costs an allocation or copy depending on how the data arrives, but it simplifies storage in long-lived services, task queues, and registries. The explicit + 'a version avoids that ownership cost but transfers the lifetime relationship into the caller's API.
Neither design is universally better. I use the lifetime error to expose the product decision: should the object remain tied to the request data, or should it become independent?
Why adding 'static to the input is usually wrong
A tempting response is changing text: &'a str into text: &'static str. This makes the example compile for string literals, but it rejects ordinary strings created at runtime. It turns a local representation problem into a much stronger caller requirement.
I only require 'static when the system truly stores the object without any enclosing lifetime, such as a process-wide registry or a spawned task that may outlive the current stack. Even then, owning the data is often more useful than demanding a static borrow.
My debugging sequence
When a trait object unexpectedly asks for 'static, I do this:
- Write the omitted bound explicitly as
dyn Trait + 'static. - Inspect the hidden concrete type for references and their real owners.
- Decide whether the returned object should borrow or own that data.
- Use
+ 'aand expose the lifetime when borrowing is intended. - Move or clone data into an owned implementation when independence is intended.
- Check every storage boundary, thread, and callback that may require a longer lifetime.
The core principle is simple: type erasure hides the concrete type, but it cannot erase the lifetime of references inside that type. Writing the object lifetime explicitly makes this invisible part of dyn Trait readable again.