Skip to content
← All Rust articles

Rust explainer

Trait objects have lifetimes too

About 8 min to read. Allow extra time to work through examples.

Every trait object type such as dyn Debug carries a lifetime bound, and when you leave it out the compiler fills in a default. For Box<dyn Trait> in a signature that default is 'static, which explains many lifetime errors in code that never mentions 'static.

This article uses Rust. You can make it your practice mode to browse related content across the site.

A trait object is a value used through a trait, written dyn Trait, where the concrete type is only known at run time. Box<dyn Error> is a box holding some value that implements Error, and &dyn Debug is a reference to some value that implements Debug.

Every trait object type also has a lifetime bound, written after a +: dyn Debug + 'a means "some Debug value that is valid for at least 'a". The bound limits which borrowed data the hidden value may contain. If the value holds a &'x str, then 'x must outlive the object's bound.

You rarely write that bound, so the compiler fills in a default. Consider:

Box<dyn Error>

In a function signature, struct field or type alias, that type means Box<dyn Error + 'static>. The 'static bound says the boxed error may not borrow anything that could be freed earlier. That default explains errors that mention 'static in code where you never wrote 'static.

The default rules

The Rust Reference calls these rules default trait object lifetime bounds. For traits that declare no lifetime bound of their own (Error, Debug, Fn and most traits you write), they reduce to this:

  1. The containing type decides first. If the trait object is a type argument of a type whose parameter has exactly one lifetime bound, that bound is used. &'a T requires T: 'a, so &'a dyn Foo gets 'a.
  2. If the containing type's parameter has two or more lifetime bounds, there is no default and you must write the bound. The compiler reports error[E0228]: cannot deduce the lifetime bound for this trait object type from context.
  3. If the containing type gives no bound, as with Box<T>, Vec<T>, Rc<T> and Arc<T>, the bound is 'static in signatures, struct fields and type aliases, and is inferred inside function bodies (see below).

When the trait itself declares a lifetime bound, for example trait Bar<'a>: 'a, extra rules apply. The Reference lists them; this article only uses traits without such bounds.

The two cases you will see most often:

Box<dyn Foo>        // means  Box<dyn Foo + 'static>
&'a dyn Foo         // means  &'a (dyn Foo + 'a)

To ask for something other than the default, write the bound yourself. In a function's return type you can write '_, which takes the lifetime chosen by the ordinary lifetime elision rules (for a function with one reference parameter, that parameter's lifetime):

Box<dyn Foo + '_>

'_ is not allowed in a struct field. There the compiler reports error[E0106]: missing lifetime specifier, and you need a named lifetime parameter on the struct instead.

Where it shows up

Returning a boxed value built from a reference:

use std::fmt::Debug;

fn wrap(s: &str) -> Box<dyn Debug> {
    Box::new(s)
}
error: lifetime may not live long enough
  returning this value requires that `'1` must outlive `'static`

Here '1 is the compiler's name for the unwritten lifetime of s. The box holds that &str, but the return type, by the default, promises a dyn Debug + 'static. The compiler's help text suggests the fix: let the object carry the parameter's lifetime.

fn wrap(s: &str) -> Box<dyn Debug + '_> {
    Box::new(s)
}

A struct field holding boxed closures:

struct Registry { handlers: Vec<Box<dyn Fn(i64) -> i64>> }

The field type means Vec<Box<dyn Fn(i64) -> i64 + 'static>>. The declaration itself compiles without a warning. The error appears where you store a closure that captures a reference:

let t = vec![1i64];
let tr: &Vec<i64> = &t;
let mut r = Registry { handlers: Vec::new() };
r.handlers.push(Box::new(move |x| x + tr[0]));
// error[E0597]: `t` does not live long enough

The move closure captures tr, a borrow of t, so the closure is only valid while t is. Storing it in the field requires the borrow to last for 'static, which a local variable cannot provide. The error is reported at the borrow of t, not at the struct, but current compilers add a note that points at the cause:

note: due to object lifetime defaults, `Box<dyn Fn(i64) -> i64>` actually means `Box<(dyn Fn(i64) -> i64 + 'static)>`

If the registry really should hold borrowing closures, give the struct a lifetime parameter and use it as the bound:

struct Registry<'a> { handlers: Vec<Box<dyn Fn(i64) -> i64 + 'a>> }

With that declaration the same four lines compile, and the borrow checker then ensures r is not used after t is dropped.

Check your understanding Why does &'a Box<dyn Foo> still contain a 'static object, when &'a dyn Foo does not? Show answer

Because the type that directly contains the trait object sets the default, and here that is the Box, not the reference.

The trait object is the type argument of Box<...>. Box<T> puts no lifetime bound on T, so the object becomes dyn Foo + 'static. The resulting Box<dyn Foo + 'static> is then the type behind &'a, and the reference's 'a does not reach inside it.

So &'a Box<dyn Foo> means &'a Box<dyn Foo + 'static>, while &'a dyn Foo means &'a (dyn Foo + 'a). A function declared to return &'a Box<dyn Foo> cannot return a &'a Box<dyn Foo + 'a>.

Separately, Clippy's borrowed_box lint, which warns by default, flags a &Box<T> parameter and suggests &T. Under a configuration that turns warnings into errors (cargo clippy -- -D warnings), that warning fails the check. Taking &dyn Foo avoids the lint and gives the 'a default.

Inside function bodies the bound is inferred

In a let type annotation inside a function body, an omitted object lifetime is inferred from how the value is used rather than fixed at 'static:

use std::fmt::Debug;

fn show() {
    let s = String::from("hi");
    let b: Box<dyn Debug> = Box::new(&s);   // compiles
    println!("{b:?}");
}

The 'static default applies to types written outside expressions: function signatures, struct and enum fields, and type aliases. If the annotation above used type DebugBox = Box<dyn Debug>; instead, the alias would carry 'static and the same line would fail with error[E0597]: s does not live long enough.

So a let that compiles is not evidence that the same type works in a signature or a field. Check those positions separately.

E0759 is no longer emitted

Searching for this error may lead you to E0759, whose explanation covers this case: why fn bar(x: &i32) -> Box<dyn Debug> fails and why Box<dyn Debug + '_> fixes it. Current compilers no longer use that code:

$ rustc --explain E0759
#### Note: this error code is no longer emitted by the compiler.

The Box<dyn Debug> part of the explanation still applies, but in your terminal you will see the uncoded error: lifetime may not live long enough shown above. Its impl Debug example is out of date: since the 2024 edition, a return-position impl Trait type may use the lifetimes of the function's parameters, so fn foo(x: &i32) -> impl Debug { x } compiles without + '_. The 'static default described in this article applies to dyn Trait, not to impl Trait.

Check your understanding Box<dyn Error> and Box<dyn Error + Send + Sync + 'static> both appear in real code. What is the difference? Show answer

In terms of the object lifetime, there is none. In a signature, Box<dyn Error> already means Box<dyn Error + 'static>, so writing 'static explicitly only makes the default visible to the reader.

The difference is Send + Sync. A Box<dyn Error> may hold an error type that cannot be moved to or shared with another thread, so the box itself is neither Send nor Sync. For example, a std::thread::spawn closure that returns Result<(), Box<dyn Error>> is rejected with error[E0277]: dyn std::error::Error cannot be sent between threads safely, because spawn requires its result to be Send. Returning Result<(), Box<dyn Error + Send + Sync>> compiles.

Both forms work with the ? operator for standard error types such as ParseIntError. Use the Send + Sync form when the error may need to cross a thread boundary, for example in a library whose callers might use threads. The plain form is enough when the error stays on one thread, as in many main functions.

Summary

you write in a signature, field or alias it means why
Box<dyn Foo> Box<dyn Foo + 'static> Box<T> puts no lifetime bound on T
&'a dyn Foo &'a (dyn Foo + 'a) &'a T requires T: 'a
&'a Box<dyn Foo> &'a Box<dyn Foo + 'static> the Box directly contains the object
Box<dyn Foo + '_> (return type) the lifetime chosen by elision explicit bound replaces the default

When an error mentions 'static and your code never wrote it, one place to check is any dyn Trait type without an explicit + 'lifetime bound.

step 18 of 22

Ownership III: Lifetimes, Explicitly

Up next Helping closures infer reference lifetimes