What is object safety

Object safety determines if a Rust trait can be used for dynamic dispatch via trait objects like `dyn Trait`.

The universal remote problem

You are building a plugin system for a text editor. You want to store different syntax highlighters in a single collection, so you reach for a trait object: Vec<Box<dyn Highlighter>>. You define the trait, add a method to apply colors, and everything compiles. Then you decide you want each highlighter to accept a custom configuration type. You change the method signature to fn apply<C: Config>(&self, config: C). The compiler immediately rejects you with E0038 (the trait cannot be made into an object). You did not change the logic. You only added a generic type parameter. Rust refuses to let you use dyn Highlighter anymore.

This is object safety. It is not a bug. It is a hard boundary between compile-time polymorphism and runtime polymorphism. Rust draws a line in the sand: if a trait method needs to know exact types at compile time, it cannot live inside a trait object. The compiler enforces this so that dynamic dispatch remains predictable, fast, and memory-safe.

How trait objects actually work

Rust handles polymorphism in two completely different ways. Generics use static dispatch. The compiler looks at every place you call a generic function, sees the concrete type, and generates a brand new copy of that function tailored to that type. It is like cutting a custom key for every lock. The code runs at full speed because the compiler knows exactly which function to call.

Trait objects use dynamic dispatch. You erase the concrete type behind a dyn Trait boundary. The compiler cannot generate a custom copy for every possible type because it does not know what types will show up at runtime. Instead, it builds a virtual method table, commonly called a vtable. The vtable is a fixed array of function pointers. Every time you call a method on a dyn Trait, the runtime looks up the correct pointer in that table and jumps to it.

A trait object is actually a fat pointer. It takes up two machine words instead of one. The first word points to the actual data. The second word points to the vtable for that specific type. This layout is fixed at compile time. The vtable must know the exact signature of every method it contains. If a method has a generic type parameter, there is no single signature. There could be a version for String, a version for u32, a version for Vec<Option<&str>>. The vtable cannot hold infinite pointers. It can only hold one.

Object safety is simply the rulebook that checks whether a trait can fit into this vtable model. If a trait passes the check, you can use dyn Trait. If it fails, you must stick to generics or redesign the trait.

The rules of object safety

A trait is object-safe if every method can be represented as a single function pointer in a vtable. The compiler checks four main conditions.

/// Object-safe trait: fixed signatures, known size.
trait Shape {
    fn area(&self) -> f64;
    fn scale(&mut self, factor: f64);
}

/// Not object-safe: generic method creates infinite signatures.
trait GenericProcessor {
    fn process<T>(&self, item: T);
}

The first rule is straightforward. Methods cannot have generic type parameters. The vtable needs one entry per method. A generic method would require an entry for every possible type T could become. The compiler cannot allocate that at compile time.

The second rule involves Self. Methods cannot return Self by value. If a method returns Self, the compiler needs to know the exact size of the return type to allocate stack space. Behind a dyn Trait boundary, Self is erased. The runtime only knows it is some type that implements the trait, but it does not know how many bytes to reserve. Returning Self breaks the fixed layout requirement.

The third rule covers size bounds. A trait cannot require Self: Sized on itself or on individual methods. The Sized trait is a built-in marker that guarantees a type has a known size at compile time. Trait objects are explicitly unsized. They live on the heap or behind references. If a method demands Self: Sized, it is asking for a guarantee that dyn Trait cannot provide.

The fourth rule handles associated types and constants. Associated types must have a concrete default or be fully resolved before the trait can be object-safe. Associated constants are generally fine, but complex bounds on them can trigger the same size or generic restrictions.

Trust the vtable. It is a fixed array of pointers. If your trait cannot fit into that array, it is not object-safe.

When the compiler says no

You will hit E0038 the moment you try to write dyn Trait for a trait that breaks the rules. The error message points directly to the offending method. It usually says something like "the trait cannot be made into an object because method process has generic type parameters."

Another common trap is E0782 (expected sized type). This happens when you try to store a trait object directly in a struct or return it by value without a pointer. Trait objects are unsized. You cannot put an unsized type on the stack. You must wrap it in a Box, &, &mut, or Rc. The compiler will not silently truncate your data. It forces you to pick a pointer type.

/// Returns a trait object behind a reference.
/// The caller must ensure the underlying data lives long enough.
fn get_renderer() -> &dyn Renderer {
    // SAFETY: Not applicable here, but demonstrates convention.
    // Convention: prefer returning &dyn Trait over Box<dyn Trait>
    // when the caller already owns the data. It avoids heap allocation.
    &static_renderer
}

There is an escape hatch for the Sized rule. You can write trait MyTrait: ?Sized { ... }. The ?Sized syntax tells the compiler that this trait does not require Self to be sized. It is the standard convention for traits that are meant to be used as trait objects. Without ?Sized, the compiler implicitly adds Self: Sized to every trait, which immediately disqualifies it from dynamic dispatch.

Convention matters here. The Rust community writes dyn Trait explicitly. The dyn keyword is not optional in modern Rust. It signals to every reader that type erasure is happening. It also prevents accidental ambiguity when a type name and a trait name collide. Write dyn every time. It costs nothing and saves debugging time.

Fixing broken traits

When a trait fails object safety, you have three practical paths. You can remove the generics, you can wrap the generic type in a trait object, or you can split the trait into a base and an extension.

Consider a command pattern for a UI framework. You want every command to execute and undo. You also want commands to accept a context that provides UI state.

/// Base trait for commands. Object-safe.
trait Command {
    fn execute(&self);
    fn undo(&self);
}

/// Extension trait for context-aware commands. Not object-safe.
trait ContextCommand {
    fn execute_with<C: Context>(&self, ctx: C);
}

The ContextCommand trait breaks object safety because of the generic C. You cannot put dyn ContextCommand in a Vec. The fix depends on your actual needs.

If the context only needs to be read, wrap it in a trait object. Change the signature to fn execute_with(&self, ctx: &dyn Context). The vtable now has a single, fixed signature. The runtime handles the dynamic dispatch for the context internally. This is the most common solution for plugin systems.

If the context must be owned or mutated, box it. Use fn execute_with(&self, ctx: Box<dyn Context>). The heap allocation moves the size uncertainty out of the method signature and into the heap. The vtable sees a pointer, which has a known size.

If you need the full power of generics for performance, keep the generic trait but do not use dyn. Store the commands in a generic collection instead: Vec<Box<dyn Command>> for the base behavior, and handle the generic parts through separate, statically dispatched helper functions. You can also use the dyn_clone crate or manual Box<dyn Trait> casting if you absolutely must mix static and dynamic dispatch, but that adds runtime overhead and complexity.

Pick the simplest fix that matches your performance requirements. Wrapping in &dyn or Box<dyn> solves 90% of object safety errors without sacrificing correctness.

Picking your dispatch strategy

Use generics when you know all the types at compile time and want zero runtime overhead. Use generics when the trait methods return Self, take generic parameters, or rely on type-specific optimizations. Use generics for internal library code where the caller controls the type parameters.

Use dyn Trait when you need to store heterogeneous types in the same collection. Use dyn Trait when types are determined at runtime, such as plugin systems, deserialization targets, or event handlers. Use dyn Trait when the trait methods have fixed signatures and do not return Self.

Use Box<dyn Trait> when you need owned, heap-allocated trait objects that outlive the current scope. Use Box<dyn Trait> when you cannot guarantee the lifetime of a reference. Use Box<dyn Trait> when the trait object must be moved between threads or stored in a struct.

Use &dyn Trait when the underlying data is owned elsewhere and you only need temporary access. Use &dyn Trait when you want to avoid heap allocation and the caller can guarantee the data lives long enough. Use &dyn Trait for function parameters that accept any type implementing the trait without taking ownership.

Use Arc<dyn Trait> when multiple threads need shared, read-only access to the same trait object. Use Arc<dyn Trait> when the trait object must survive across async boundaries or be cloned into multiple tasks. Use Arc<dyn Trait> when you already have Arc elsewhere in your architecture and want to avoid double indirection.

Match the dispatch strategy to the lifetime and ownership requirements. Static dispatch is faster. Dynamic dispatch is more flexible. Rust forces you to choose explicitly.

Where to go next