The missing keyword that changed everything
You are building a command parser. Different commands need different handlers. You want to store them in a single list so you can iterate and execute them later. You write Vec<Box<Command>> and the compiler immediately stops you. The message is blunt: trait objects must include the dyn keyword. You did not ask for a keyword. You just wanted a list of things that implement Command.
The error is not a syntax tax. It is a compiler instruction to perform type erasure. When you write Box<Command>, Rust has to guess what you mean. Are you asking for a box that holds exactly one concrete type that happens to implement Command? Or are you asking for a box that can hold any type implementing Command, with the specific type hidden behind a standard interface?
Think of a theater ticket. A ticket for seat 14B is a concrete type. You know exactly where you sit. A general admission wristband is a trait object. It does not care which specific person wears it. It only cares that the wristband fits the standard clasp. The dyn keyword tells the compiler to hand out wristbands instead of seat assignments. It strips away the concrete type information and replaces it with a standardized lookup table.
Add dyn and the compiler stops guessing. You are only clarifying intent.
How type erasure actually works
The compiler rejects bare trait syntax in type positions. You must prefix the trait name with dyn to signal dynamic dispatch.
Here is the smallest case that triggers the error and the exact fix.
use std::fmt::Display;
// The compiler rejects this. It cannot tell if `Display` is a concrete type or a trait.
// let item: Box<Display> = Box::new(42);
// Adding `dyn` makes the intent explicit. We want a dynamic trait object.
let item: Box<dyn Display> = Box::new(42);
/// Prints any value that implements Display, regardless of its concrete type.
fn show_value(val: &dyn Display) {
// The reference is also a fat pointer. It carries the vtable alongside the data address.
println!("{}", val);
}
The fix is mechanical. Search your codebase for Box<TraitName>, &TraitName, or &mut TraitName where TraitName is a trait. Prepend dyn. The compiler will stop complaining. The behavior of your program does not change. You are only clarifying intent.
Trust the keyword. It tells the compiler exactly how to lay out memory.
Under the hood: fat pointers and vtables
When you compile Box<dyn Display>, Rust does not store a Box pointing directly to an i32. It stores a fat pointer. A fat pointer contains two pieces of data. The first is a pointer to the actual value on the heap. The second is a pointer to a virtual method table, commonly called a vtable.
The vtable is a static array of function pointers generated by the compiler. Each entry corresponds to a method in the Display trait. If Display has one method, the vtable has one slot. If you add a method to the trait later, the vtable grows. The compiler rebuilds the table for every concrete type that implements the trait.
At runtime, calling val.to_string() does not jump to a single memory address. The CPU follows the data pointer to find the value. It follows the vtable pointer to find the correct to_string function for that specific type. It then executes the indirect jump. This is dynamic dispatch. It happens at runtime, not compile time.
Rust 2015 allowed bare trait syntax like Box<Trait>. It worked, but it created ambiguity. As the language grew, impl Trait and generic type parameters became common. Writing Box<Trait> started colliding with the mental model of generics. The 2018 edition made the distinction mandatory. If you want dynamic dispatch, you write dyn. If you want static dispatch, you use generics or impl Trait. The compiler no longer guesses.
Convention aside: the Rust community treats dyn as a boundary marker. You place it at the edges of your system where types diverge. Inside performance-critical modules, you keep things concrete or generic. You do not sprinkle dyn throughout your codebase just to avoid writing type parameters.
Treat dyn as a deliberate trade-off. You are trading compile-time certainty for runtime flexibility. Make that trade consciously.
Realistic example: a heterogeneous plugin list
Consider a plugin system where each plugin implements a Plugin trait. You want to load them dynamically and store them in a configuration struct.
Here is how the trait and its implementations look.
/// Defines the interface all plugins must implement.
trait Plugin {
fn initialize(&self);
fn run(&self);
}
struct DatabasePlugin;
struct CachePlugin;
impl Plugin for DatabasePlugin {
// Concrete implementation for database operations.
fn initialize(&self) { println!("DB connected"); }
fn run(&self) { println!("Running queries"); }
}
impl Plugin for CachePlugin {
// Concrete implementation for cache operations.
fn initialize(&self) { println!("Cache warmed"); }
fn run(&self) { println!("Serving cache"); }
}
Here is how the application container stores and executes them.
/// Holds a list of heterogeneous plugins.
struct App {
// `dyn` is required here because the Vec contains different concrete types.
// Without it, the compiler cannot allocate a single contiguous block of memory.
plugins: Vec<Box<dyn Plugin>>,
}
impl App {
fn new() -> Self {
let mut plugins = Vec::new();
// Each push boxes a different concrete type.
// The compiler erases the concrete type and stores a fat pointer.
plugins.push(Box::new(DatabasePlugin));
plugins.push(Box::new(CachePlugin));
App { plugins }
}
fn boot(&self) {
// Iterating over trait objects uses dynamic dispatch for each method call.
// The CPU looks up the correct function pointer in each plugin's vtable.
for plugin in &self.plugins {
plugin.initialize();
plugin.run();
}
}
}
The code compiles and runs. Each Box<dyn Plugin> occupies the same amount of memory, regardless of whether it holds a DatabasePlugin or a CachePlugin. The size is fixed at two pointers. This uniformity is what allows Vec to store them contiguously.
The uniform pointer size is what makes heterogeneous collections possible. Without it, the compiler cannot allocate a single contiguous block of memory.
Pitfalls and the Sized trap
The error appears whenever you use a trait name in a type position without dyn. This happens in function signatures, struct fields, and closure return types. The compiler cannot infer dynamic dispatch from context alone.
A common trap is mixing dyn with impl Trait. You cannot write fn get_plugin() -> impl dyn Plugin. The impl Trait syntax already promises a single concrete type that implements the trait. Adding dyn contradicts that promise. impl Trait uses static dispatch. dyn Trait uses dynamic dispatch. They solve different problems.
Another pitfall involves the Sized trait. Every type in Rust has a known size at compile time by default. Trait objects do not. The compiler does not know how big a dyn Plugin is, because it could be anything. That is why dyn Trait is always !Sized. You cannot put a dyn Trait on the stack. You must put it behind a pointer like Box, Rc, Arc, or &.
If you try to write fn process(p: dyn Plugin), the compiler rejects it. You must write fn process(p: &dyn Plugin) or fn process(p: Box<dyn Plugin>). The pointer carries the size information. The trait object itself remains unsized.
Performance is the final consideration. Dynamic dispatch prevents the compiler from inlining methods. The CPU must follow an extra pointer and perform an indirect jump. In a tight loop processing thousands of items, this indirection adds up. Branch prediction struggles with indirect calls. If you know the exact type at compile time, generics will always outperform trait objects.
Convention aside: when you run cargo fix, it automatically inserts dyn in most cases. The tool is reliable for this specific migration. You still need to review the changes. Automated fixes sometimes place dyn in places where a generic or impl Trait would be more appropriate. Trust the tool for syntax. Trust your judgment for architecture.
Never reach for trait objects just to avoid writing a type parameter. The runtime cost is real.
Choosing your dispatch strategy
Use dyn Trait when you need to store multiple different types in the same collection or pass a value across a boundary where the concrete type is unknown. Use dyn Trait when building plugin architectures, UI component trees, or serialization frameworks where heterogeneity is the goal. Use impl Trait when a function returns a single concrete type but you want to hide that type from the caller. Use generic type parameters (T: Trait) when you want maximum performance and compile-time monomorphization. Reach for references (&dyn Trait) when you need to read shared state without taking ownership.
Pick the dispatch strategy that matches your data shape, not your convenience.