Skip to content
← All articles

Trait objects have lifetimes too

The most surprising set of implicit rules in the language: a lifetime you never wrote, defaulting to `'static`, silently rejecting your program. You hit it the moment you box a closure or return a `Box<dyn Error>`.

Box<dyn Error>

There is a lifetime in that type. You did not write it, nothing on the page hints at it, and it is 'static.

This is the most surprising set of implicit rules in Rust, and you meet them the first time you box a closure or return a boxed error. The errors it produces mention 'static and point at code that contains no lifetimes at all.

The default rules

A dyn Trait always has a lifetime bound. When you do not write one, the Reference’s object lifetime defaults supply it, in this order:

  1. If the trait itself declares a lifetime bound, that is used. For trait Foo: 'a, a dyn Foo gets 'a.
  2. Otherwise the containing type decides. Specifically: if the type that contains the trait object imposes exactly one lifetime bound on its parameter, that bound is used.
  3. If more than one bound applies and one of them is 'static, 'static wins.
  4. If nothing applies, the default is 'static in type positions.

In practice, two cases account for almost everything you will meet:

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

Why the difference? Box<T> is declared with no lifetime bound on T — it owns its contents and has no opinion about how long they live — so rule 4 fires and you get 'static. &'a T structurally requires T: 'a, so rule 2 fires and the object inherits 'a.

And you can always opt out and let inference do it:

Box<dyn Foo + '_>

Where it bites

Returning a boxed thing built from a reference:

use std::fmt::Debug;

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

Note the shape of that message. It says 'static. Your function contains no 'static, no 'a, no lifetime of any kind. The 'static came from the default rule, and the fix is to opt out of it:

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

A struct field holding boxed closures:

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

That field type means + 'static and produces no warning at all. Push a closure that captures a reference into it and the error lands somewhere else entirely:

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 complaint is about t, a Vec that is alive for the whole function. The actual cause is a 'static bound three lines away in a struct declaration that contains no lifetimes. That invisibility is the lesson. The fix is to give the struct a lifetime parameter and spell the bound out:

struct Registry<'a> { handlers: Vec<Box<dyn Fn(i64) -> i64 + 'a>> }
💡Why does &'a Box<dyn Foo> still contain a 'static object, when &'a dyn Foo does not? click to reveal

Because the innermost containing type sets the bound, and here that is the Box, not the reference.

Work outward. The trait object sits inside Box<...>. Box<T> imposes no lifetime bound on T, so the default rule fires and the object becomes dyn Foo + 'static. Only then is the whole Box<dyn Foo + 'static> placed behind a &'a. The outer reference’s 'a never gets a chance to influence the object — the question was already settled one level in.

So &'a Box<dyn Foo> means &'a Box<dyn Foo + 'static>, while &'a dyn Foo means &'a (dyn Foo + 'a). Adding a Box in the middle changes the meaning of the innermost part of the type, which is genuinely counterintuitive and catches experienced people.

A side note you will hit in review: &Box<T> as a parameter type also trips clippy::borrowed_box“you seem to be trying to use &Box<T>. Consider using just &T — which is warn-by-default and fatal here. Taking &dyn Foo instead both silences the lint and gives you the more useful default.

The one place the default does not apply

Inside a function body, a let annotation can have its object lifetime inferred rather than defaulted:

let s = String::from("hi");
let b: Box<dyn Debug> = Box::new(&s);   // compiles

This surprises people who have just learned the rule and expect 'static. The defaults are about type positions in signatures and declarations — function signatures, struct fields, type aliases — where there is nobody to infer from. In an expression, there is.

Do not build a mental model on the let case. Test your understanding against signatures and struct fields, which is where the rule actually governs and where every real bug lives.

The error code that no longer exists

If you search for the boxed-closure error you will land on E0759, whose explanation page is genuinely the best writing on this topic anywhere. It explains exactly why fn foo(x: &i32) -> Box<dyn Debug> fails and why Box<dyn Debug + '_> is the fix.

It is also dead:

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

Read the page — it is worth it — but expect the bare error: lifetime may not live long enough in your terminal, and do not waste time looking for the code in your output.

💡Box<dyn Error> and Box<dyn Error + Send + Sync + 'static> both appear constantly in real code. What is the difference, and why is one of them so much longer than it needs to be? click to reveal

In terms of the object lifetime, there is no difference: Box<dyn Error> already means Box<dyn Error + 'static>, so the explicit 'static in the second is redundant. It is written out for emphasis, and because the surrounding + Send + Sync makes the whole bound list feel like something you should spell out.

The real difference is Send + Sync. Box<dyn Error> can hold an error that is not safe to move between threads or share across them. Box<dyn Error + Send + Sync> can be returned from a spawned thread, stored in a shared error channel, or held by a 'static future — which is why every async runtime and most library APIs use the longer form.

The practical guidance: use Box<dyn Error + Send + Sync + 'static> in library code that might be used from threaded contexts, and Box<dyn Error> in an application’s main where nothing crosses a thread boundary. The 'static is optional noise either way — but written down, it does at least remind the reader that a boxed error cannot borrow from anything.

The card

you write it means why
Box<dyn Foo> Box<dyn Foo + 'static> Box<T> bounds nothing
&'a dyn Foo &'a (dyn Foo + 'a) &'a T requires T: 'a
&'a Box<dyn Foo> &'a Box<dyn Foo + 'static> innermost container wins
Box<dyn Foo + '_> inferred explicit opt-out

And the habit: when an error mentions 'static and your code contains no 'static, look for a dyn.