Rust Error E0038 The Trait Cannot Be Made Into an Object
E0038 means a trait is not dyn compatible, so you cannot write dyn Trait for it. Learn the rules that break it and four ways to restore object safety.

error[E0038]: the trait ... is not dyn compatible appears when you write Box<dyn Something> for a trait that cannot be turned into a trait object. Older compilers worded it "the trait cannot be made into an object", which is still what people search for. The message always includes a line starting ...because, and that line names the cause, so the fix is a lookup: find the cause below, apply its repair. This post covers the five causes in the order you are likely to meet them, with the compiler's real text from Rust 1.99, and a repaired version of each.
If you want the full rule set and the reasoning behind it, read dyn compatibility. This page is the fast path from the error to a compiling program.
Cause one: an async fn in the trait
In 2026 this is the most common, because writing async fn in a trait became stable and works well for generics, and then fails when the trait is stored as dyn:
trait Repository {
async fn find(&self, id: u32) -> Option<String>;
}
fn main() {
let repo: Box<dyn Repository> = Box::new(Memory);
}error[E0038]: the trait `Repository` is not dyn compatible
--> d6.rs:14:23
|
14 | let repo: Box<dyn Repository> = Box::new(Memory);
| ^^^^^^^^^^ `Repository` is not dyn compatible
|
note: for a trait to be dyn compatible it needs to allow building a vtable
1 | trait Repository {
| ---------- this trait is not dyn compatible...
2 | async fn find(&self, id: u32) -> Option<String>;
| ^^^^ ...because method `find` is `async`An async fn returns an anonymous future type whose size differs per implementation, so the vtable cannot hold a fixed signature. The fix is to return a boxed future yourself, which has one concrete type:
use std::future::Future;
use std::pin::Pin;
trait Repository {
fn find<'a>(&'a self, id: u32) -> Pin<Box<dyn Future<Output = Option<String>> + 'a>>;
}
struct Memory;
impl Repository for Memory {
fn find<'a>(&'a self, id: u32) -> Pin<Box<dyn Future<Output = Option<String>> + 'a>> {
Box::pin(async move { Some(format!("row {id}")) })
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let repo: Box<dyn Repository> = Box::new(Memory);
println!("{:?}", repo.find(7).await);
}Some("row 7")The trait method now returns Pin<Box<dyn Future<Output = Option<String>> + 'a>>, the implementer wraps its body in Box::pin(async move { ... }), and the call site awaits as before. The cost is one heap allocation per call. The async-trait crate generates exactly this boilerplate with an attribute if you would rather not write it. If you control all the callers and do not need dyn, generics with a plain async fn in the trait are allocation-free, so choose the boxed form only where the object is needed.
Cause two: a generic method
The message: ...because method visit has generic type parameters. A method fn visit<T: Debug>(&self, item: T) would need a vtable entry per T. Replace the type parameter with a trait object:
trait Visitor {
fn visit(&self, item: &dyn Debug);
}Callers pass &value, and the method keeps its meaning. If the generic method matters only for concrete types, leave it and mark it where Self: Sized, which hides it from the object while keeping it available on the concrete type.
Cause three: a method that returns Self
The message: ...because method duplicate references the Self type in its return type. The caller of a method on a dyn value does not know Self's size, so it cannot receive one. Return a box of the trait object, the clone_box pattern:
trait Shape {
fn area(&self) -> f64;
fn clone_box(&self) -> Box<dyn Shape>;
}
impl Clone for Box<dyn Shape> {
fn clone(&self) -> Self {
self.clone_box()
}
}Each implementer writes Box::new(self.clone()) once. A supertrait of Clone on your trait is the same problem in disguise, and the real standard library message for it is shorter:
error[E0038]: the trait `Clone` is not dyn compatible
--> d5.rs:2:28
|
2 | let items: Vec<Box<dyn Clone>> = vec![Box::new(1), Box::new("a")];
| ^^^^^ `Clone` is not dyn compatible
|
= note: the trait is not dyn compatible because it requires `Self: Sized`Clone requires Sized, so no trait that has it as a supertrait can be an object. The dyn-clone crate packages the clone_box pattern if you need it often.
Cause four: an associated function with no self
The message: ...because associated function new has no self parameter. A constructor like fn new(name: &str) -> Self is called on a type and not on a value, so there is nothing to find the vtable from. The compiler's own help text gives the repair:
help: alternatively, consider constraining `new` so it does not apply to trait objects
|
2 | fn new(name: &str) -> Self where Self: Sized;
| +++++++++++++++++Add where Self: Sized to that one method. The trait becomes dyn compatible, Dog::new("rex") still works because Dog is sized, and the object offers the other methods. The alternative help, "give it a &self argument", is rarely what you want for a constructor.
Cause five: an associated constant
The message: ...because it contains associated const LEGS`. Constants belong to a type, and a trait object has no type to read one from. Expose the value as a method, fn legs(&self) -> u32`, which can be looked up through the vtable:
trait Animal {
fn name(&self) -> String;
fn legs(&self) -> u32;
}If you also want a compile-time constant for generic code, keep it on a separate small trait that is not used as an object.
How to tell which one you have
Do not guess from the trait. Look for the line beginning ...because and match it against the five above:
The ...because text | Cause | Repair |
|---|---|---|
method x is async | Cause one | Return Pin<Box<dyn Future>>, or use async-trait |
method x has generic type parameters | Cause two | Take &dyn Trait, or add where Self: Sized |
references the Self type in its return type | Cause three | Return Box<dyn Trait>, or add where Self: Sized |
associated function x has no self parameter | Cause four | Add where Self: Sized |
contains associated const X | Cause five | Make it a method |
the trait requires Self: Sized | A supertrait such as Clone | Drop the supertrait, or wrap with a clone_box method |
A trait can break several rules at once; the compiler reports the first, and after you fix it the next appears. Fix them one at a time and rerun cargo check.
When the real fix is to avoid dyn
If the compiler's list of repairs is getting long, consider whether you need a trait object at all. When there is a small closed set of implementations, an enum that wraps them and implements the trait by match avoids every dyn-compatibility rule and is faster, as measured in static versus dynamic dispatch. When the set is open but you control the callers, make the surrounding code generic over T: Trait and skip the object entirely. And if the trait genuinely has two jobs, split it, so that the object-facing half is dyn compatible by construction. The longer treatment of all of these is in trait objects.
Confirming the repair worked
After each change, rerun cargo check and read the next error, not just whether the first one vanished. A compiling Box<dyn Trait> declaration is only half the job: the code that calls through the object can still fail. For instance, a method you hid with where Self: Sized cannot be called on the object at all, and the compiler will say "the new method cannot be invoked on a trait object" at that call. That is expected. It means the method is for the concrete type, and the call should be made before the value is boxed. Add one small test that boxes each implementer and calls every method through the object, so a later change to the trait that breaks dyn compatibility fails in your test suite and not in a downstream crate.


