Trait Object Safety

The complete rules that decide whether a Rust trait can become a trait object, why those rules exist, and how to design traits that stay usable with dynamic dispatch.

Not every trait can be turned into a dyn Trait. The concept of object safety — recently renamed dyn compatibility in Rust — is the set of conditions a trait must satisfy to be used as a trait object. If a trait violates these conditions, the compiler rejects attempts like Box<dyn MyTrait> with error E0038.

This section explains what makes a trait object‑safe, why those restrictions exist, and how to work with them. It assumes you already understand that trait objects enable dynamic dispatch via a virtual table (vtable); the focus here is on what the vtable demands from a trait’s definition.

The Underlying Mechanism

A trait object pointer (e.g., &dyn Show) is a fat pointer: it stores a data pointer and a vtable pointer. The vtable is a static array of function pointers, one per dispatchable method, laid out identically for every concrete type that implements the trait. When the compiler builds the vtable for impl Show for String, it writes a pointer to <String as Show>::show. When someone calls x.show() through a &dyn Show, the runtime reads that pointer and calls it.

This mechanism works only if every method’s type signature is known without knowing the erased concrete type. The vtable entry must have a fixed function‑pointer type — something like fn(*const ()) -> String. Anything that depends on the concrete Self type (other than the receiver) or that introduces generics would need a different vtable layout or an infinite number of vtable entries. That is why object‑safety rules exist: they are exactly the rules that keep the vtable unambiguous.

The Object‑Safety Rules

A trait is object‑safe if all of the following hold. Each rule corresponds to a reason the vtable would become impossible or unsound.

Rule 1 — No Self: Sized supertrait

The trait must not require Self: Sized as a supertrait:

// NOT object‑safe
trait Widget: Sized {
    fn draw(&self);
}

dyn Widget itself is an unsized type, so it can never implement Sized. Making Sized a supertrait immediately blocks the trait from being used as a trait object — the compiler won’t even try.

Immediate error:

A supertrait of Sized makes the trait permanently unusable with dyn. The error appears even when you do not yet have a trait object in scope.

Rule 2 — All dispatchable methods must be “vtable‑safe”

Every method that does not carry a where Self: Sized bound must satisfy all of the following:

2a. No generic type parameters (except lifetime parameters)

trait Parser {
    // NOT dispatchable: generic type parameter T
    fn parse<T>(&self, input: &str) -> T;
}

If this method were allowed on a trait object, the compiler would need a vtable entry for every possible T — and that set is infinite. The same rule applies to impl Trait in argument position (which is syntactic sugar for a generic type parameter).

2b. Self may only appear as the receiver

The return type must not contain Self, and no parameter other than the receiver may use Self. The receiver can be self, &self, &mut self, Box<Self>, Rc<Self>, Arc<Self>, Pin<Box<Self>>, and similar pointer wrappers.

trait CloneLike {
    // NOT dispatchable: returns Self
    fn clone(&self) -> Self;
}
trait Merger {
    // NOT dispatchable: parameter uses Self
    fn merge(&self, other: Self) -> Self;
}

When someone calls .clone() on a &dyn CloneLike, they would get back a value of the erased type — the caller’s code has no idea what concrete type to expect. The vtable cannot carry a function pointer that “returns whatever the erased type is”.

Returns often break object safety:

A method returning Self is the single most common reason a trait fails to be object‑safe. The compiler error appears only when you try to use dyn, not when you define the trait.

2c. No unsized restrictions on Self beyond ?Sized

A method bound where Self: Sized is explicitly allowed — and it excludes that method from the vtable. The trait remains object‑safe, but you cannot call that particular method through a trait object.

trait Reporter {
    fn report(&self) -> String;                    // dispatchable
    fn boxed_clone(&self) -> Box<dyn Reporter>
        where Self: Sized;                         // NOT in the vtable
}

boxed_clone cannot be called on dyn Reporter because dyn Reporter isn’t Sized, but the trait as a whole is still object‑safe. This is a deliberate design pattern: you keep the trait usable as a trait object while providing convenience methods that require the concrete type.

Rule 3 — Associated constants must have defaults

A trait with an associated constant that lacks a default value is not object‑safe:

trait Configurable {
    // NOT object‑safe: no default value
    const MAX_SIZE: usize;
    fn apply(&self);
}

A vtable does not store a constant value, and the compiler cannot conjure one up from an erased type. If you provide a default, the trait becomes object‑safe again:

trait Configurable {
    const MAX_SIZE: usize = 1024;   // now fine
    fn apply(&self);
}

Rule 4 — Associated types are allowed, but must be constrained

Associated types do not make a trait non‑object‑safe, but they must be specified when forming the trait object type:

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}
// Allowed when you pin the associated type:
fn collect_ints(iter: &mut dyn Iterator<Item = i32>) -> Vec<i32> {
    let mut v = Vec::new();
    while let Some(x) = iter.next() {
        v.push(x);
    }
    v
}

If an associated type has a default, you can omit it and use plain dyn Trait.

Async methods

An async fn in a trait returns an impl Future that is specific to the concrete implementor. Because that future type depends on Self, async methods are not object‑safe by default. The async_trait crate works around this by boxing the future, effectively erasing the dependent type.

Examples of Violations and Fixes

The following tabs show a non‑object‑safe trait definition, the exact compiler output, and how to correct it.

trait Drawable {
    fn draw(&self);
    fn duplicate(&self) -> Self;          // returns Self
    fn process<T: std::fmt::Display>(&self, val: T); // generic method
}
// Error: cannot be made into an object
// fn show_all(items: Vec<Box<dyn Drawable>>) {}  // E0038

The fixed version is object‑safe. duplicate is no longer part of the vtable; process now takes a &dyn Display instead of a generic parameter.

How to Determine Object Safety Step‑by‑Step

When you encounter an E0038 error or design a new trait, a systematic check avoids guesswork.

1

Step 1: Look at the trait’s supertraits

If any supertrait is Sized, the trait cannot become a trait object. Remove the Sized bound.

2

Step 2: Inspect each method signature

For every method without a where Self: Sized bound, confirm:

  • No generic type parameters (lifetime parameters are fine).
  • Self does not appear in the return type or in any non‑receiver parameter.
  • No impl Trait in argument position (sugar for generic).
3

Step 3: Check associated items

  • Associated constants must have defaults.
  • Associated types are okay, but you will need to specify them when constructing dyn Trait<Assoc = ...>.
4

Step 4: Attempt to compile `dyn MyTrait`

Write a quick test function that accepts &dyn MyTrait. If it compiles, the trait is object‑safe.

Green light:

If the compiler accepts Box<dyn MyTrait>, your trait is object‑safe and ready for dynamic dispatch.

Real‑World Trait Spotlights

Knowing which standard library traits are object‑safe helps you internalise the rules.

  • std::fmt::Display — object‑safe. It has one method, fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result, with no generics and no Self outside the receiver. You can freely use &dyn Display.
  • std::clone::Clonenot object‑safe because fn clone(&self) -> Self. The crate dyn-clone offers a workaround via DynClone, a trait that returns Box<dyn DynClone> and is explicitly object‑safe.
  • std::ops::Fn() — object‑safe (with call syntax). You can write Box<dyn Fn(i32) -> i32>. The closure traits are designed for dynamic dispatch.
  • std::iter::Iterator — object‑safe only when you fix the associated type: dyn Iterator<Item = u32>. Without specifying Item, you can’t form the trait object.
  • std::any::Any — object‑safe. It requires 'static but not Sized, and its methods do not reference Self outside the receiver.

Designing for Object Safety

If your library intends to offer a trait that users may want to put behind dyn, design it to be object‑safe from the beginning. A few practical guidelines:

  1. Keep methods small and focused. Avoid generics on methods; if a method needs flexibility, accept a trait object instead of a generic parameter.
  2. Use where Self: Sized as an escape hatch. Put every method that needs Self (for example, a constructor or a builder method) behind this bound. The trait stays object‑safe, and concrete‑type users can still call the method.
  3. Provide a companion trait if necessary. For example, the Clone trait is not object‑safe; you can define an object‑safe CloneBox trait alongside it.
  4. Favour associated types with defaults over constants. An associated type with a default keeps the door open for trait objects; an associated constant without a default closes it.

Where Self: Sized is not a loophole:

A where Self: Sized method is invisible to the vtable, so you lose the ability to call it through a trait object. That is exactly the trade‑off — you cannot have both full Self knowledge and type erasure.

Summary

Object safety is the set of restrictions that Rust enforces to make dynamic dispatch physically possible: every method in the vtable must be callable without knowing the erased concrete type. The rules are logical consequences of that requirement — no generic methods, no Self in return types or arbitrary parameters, no unsized supertraits, and no un‑defaulted associated constants.