How do lifetimes work with trait objects

Every dyn Trait carries a hidden lifetime bound. References default it to the borrow's lifetime; Box<dyn Trait> defaults to 'static. Add + 'a when the trait object needs to borrow.

A subtle corner of the type system

You're writing a function that takes "anything that implements Display." You write &dyn Display and the compiler accepts it. Then later you store one in a struct, and suddenly the compiler is talking about lifetimes you didn't write. Or you put a Box<dyn Trait> somewhere and a borrow checker error shows up about 'static. The references that used to be invisible are now front and centre, and you need to know what's actually going on.

Trait objects and lifetimes interact in a way that's mostly hidden from you in simple cases and gets quite explicit in advanced ones. The short version: every trait object has a lifetime attached, even when you don't see it. The compiler infers it for you in obvious places. When the inference can't figure out what you meant, you have to say it out loud.

What a trait object actually is

When you write Box<dyn Display> or &dyn Display, you're not storing a concrete type. You're storing two pointers in a fat pointer pair: one pointing at the data, the other pointing at a vtable. The vtable is a small table of function pointers ("here's fmt, here's the destructor"), generated by the compiler for each concrete type that implements the trait.

That's nice for flexibility. The catch: the data pointer points at something. That something has to live somewhere. Either it lives behind a & borrow, in which case it must outlive the borrow, or it lives behind a Box, in which case the heap allocation lives until the box is dropped. Either way, there's a lifetime.

The implicit lifetime

In a function signature like:

// What you wrote
fn show(item: &dyn std::fmt::Display) {
    println!("{item}");
}

The compiler treats it as if you wrote:

// What the compiler hears
fn show<'a>(item: &'a (dyn std::fmt::Display + 'a)) {
    println!("{item}");
}

The + 'a is the trait object's own lifetime: a bound that says "the type behind this trait object lives at least as long as 'a." For a &dyn Trait or &mut dyn Trait, the default trait object lifetime is the same as the reference's lifetime, so the elision rules cover you.

For owned trait objects like Box<dyn Trait>, the rule is different. The default is 'static. So Box<dyn Display> actually means Box<dyn Display + 'static>. The implication: anything you put inside that Box must own all its data, with no borrowed references that could expire. If you try to box up a struct holding a &str that points at a local string, the compiler complains.

Seeing the default in action

Let's watch the implicit 'static bite:

use std::fmt::Display;

// Returns a boxed trait object. By default, this is Box<dyn Display + 'static>.
fn name_card(name: &str) -> Box<dyn Display> {
    // BUG: name is borrowed; it doesn't satisfy the 'static default.
    Box::new(name)
}

The compiler error:

error[E0759]: `name` has lifetime `'1` but it needs to satisfy a `'static` lifetime requirement
  --> src/main.rs:5:14
   |
5  |     Box::new(name)
   |              ^^^^ requires that `'1` must outlive `'static`

Two ways to fix this. Either keep the borrowed lifetime, by making it explicit:

// Now the boxed object is allowed to live exactly as long as `name`.
fn name_card<'a>(name: &'a str) -> Box<dyn Display + 'a> {
    Box::new(name)
}

Or own the data so no borrow is involved:

fn name_card(name: &str) -> Box<dyn Display> {
    // Allocating an owned String detaches us from the input lifetime.
    Box::new(name.to_string())
}

Both work. Which to pick depends on whether you can afford the allocation.

Storing trait objects in structs

The same rules apply to struct fields, and the elision rules don't help you there. If you put a trait object in a struct, you have to say its lifetime:

use std::fmt::Display;

// The 'a parameter says: "this struct borrows something, and the trait object
// behind the reference is also valid for at least 'a." Without 'a, the field
// would default to 'static and refuse anything borrowed.
struct Printer<'a> {
    item: &'a dyn Display,
}

impl<'a> Printer<'a> {
    fn show(&self) {
        println!("{}", self.item);
    }
}

fn main() {
    let s = String::from("hello");
    let p = Printer { item: &s };
    p.show();
}

If you want a struct that owns its trait objects via Box, you usually want either the default 'static:

// Every Box<dyn Display> stored here must contain something that owns its data.
struct Printer {
    item: Box<dyn Display>,
}

Or an explicit non-static lifetime if you really need to borrow:

struct Printer<'a> {
    item: Box<dyn Display + 'a>,
}

That + 'a on the trait object is the tell. It says: "this dyn Display doesn't have to be 'static, but it has to last at least 'a."

Why the defaults are different

Why is &dyn Trait defaulted to the reference's lifetime, while Box<dyn Trait> defaults to 'static? The reasoning is about likely intent. A reference is already tied to a specific borrow window; tying the trait object to the same window matches what you almost always want. A Box, on the other hand, is meant to outlive most things; defaulting to 'static means if you write Box<dyn Trait> casually, you can pass it around freely without lifetime parameters chasing you everywhere. Most boxed objects own their data anyway, so 'static is usually fine.

The same defaults apply to Rc<dyn Trait>, Arc<dyn Trait>, and &mut dyn Trait. References get the surrounding reference's lifetime; smart pointers get 'static. When in doubt, write the lifetime out and let the compiler tell you if you over-constrained it.

A more realistic example: callback storage

Here's where this comes up in real code. You're storing a list of closures to call later:

// Each closure may borrow something from the surrounding scope. We tie the
// whole struct to that scope's lifetime 'a so the borrows stay valid.
struct EventBus<'a> {
    listeners: Vec<Box<dyn FnMut() + 'a>>,
}

impl<'a> EventBus<'a> {
    fn new() -> Self {
        Self { listeners: Vec::new() }
    }

    // The lifetime 'a leaks into here. Anything we add must outlive 'a.
    fn subscribe(&mut self, f: impl FnMut() + 'a) {
        self.listeners.push(Box::new(f));
    }

    fn fire(&mut self) {
        for cb in &mut self.listeners {
            cb();
        }
    }
}

fn main() {
    let mut counter = 0;
    let mut bus = EventBus::new();

    // The closure borrows `counter`. The bus is tied to that borrow.
    bus.subscribe(|| counter += 1);

    bus.fire();
    bus.fire();
    drop(bus); // must drop before reading counter, since counter was borrowed
    println!("{counter}");
}

If EventBus had defaulted to Box<dyn FnMut() + 'static>, the closure capturing &mut counter would have been rejected because counter doesn't live forever. The explicit 'a lets the borrow checker thread the needle.

Common pitfalls

Hitting + 'static errors when storing a borrowed thing in a Box. Add + 'a to the trait object and a matching parameter on your struct or function.

Forgetting that dyn Trait itself isn't a type, it's an unsized one. You always need it behind a pointer (&, Box, Rc, Arc). Bare dyn Trait won't compile in most places.

Mixing trait objects with generics. If you find yourself writing fn foo<T: Trait>(x: &T) and then converting to &dyn Trait later, ask whether you needed the generic at all. Trait objects are slower (vtable indirection) but compile faster and produce smaller binaries. Generics inline the dispatch but blow up code size. Pick based on the use case.

The error the trait bound 'X: 'static' is not satisfied. Almost always means you put a borrowed value somewhere a 'static was assumed. Add an explicit lifetime to the trait object.

When to reach for what

Use &dyn Trait for short-lived dispatch where the caller already owns the data. Use Box<dyn Trait> when you want to own the trait object outright, with the default 'static bound. Add + 'a only when you need the trait object to borrow from somewhere narrower than the whole program. Use generics with impl Trait or T: Trait when you want zero-cost monomorphization and don't need a heterogeneous collection.

Where to go next