Traits

79. #[diagnostic::on_unimplemented] — Custom Error Messages for Your Traits

Trait errors are notoriously cryptic. #[diagnostic::on_unimplemented] lets you replace the compiler’s default “trait bound not satisfied” with a message that actually tells the user what went wrong.

The problem

You define a trait, someone forgets to implement it, and the compiler spits out a wall of generics and trait bounds that even experienced Rustaceans have to squint at:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
trait Storable {
    fn store(&self, path: &str);
}

fn save<T: Storable>(item: &T) {
    item.store("/data/output");
}

fn main() {
    save(&42_i32);
    // error[E0277]: the trait bound `i32: Storable` is not satisfied
}

For your own code that’s fine — but if you’re writing a library, your users deserve better.

The fix

Annotate your trait with #[diagnostic::on_unimplemented] and the compiler will use your message instead:

1
2
3
4
5
6
7
8
#[diagnostic::on_unimplemented(
    message = "`{Self}` cannot be stored — implement `Storable` for it",
    label = "this type doesn't implement Storable",
    note = "all types passed to `save()` must implement the `Storable` trait"
)]
trait Storable {
    fn store(&self, path: &str);
}

Now the error reads like documentation, not like a stack trace.

It works with generics too

The placeholders {Self} and {A} (for generic params) let you generate targeted messages:

1
2
3
4
5
6
7
#[diagnostic::on_unimplemented(
    message = "cannot serialize `{Self}` into format `{F}`",
    note = "see docs for supported format/type combinations"
)]
trait Serialize<F> {
    fn serialize(&self) -> Vec<u8>;
}

If someone tries to serialize a type for an unsupported format, they get a message that names both the type and the format — no guessing required.

Multiple notes

You can attach several note entries, and each one becomes a separate note in the compiler output:

1
2
3
4
5
6
7
8
#[diagnostic::on_unimplemented(
    message = "`{Self}` is not a valid handler",
    note = "handlers must implement `Handler` with the appropriate request type",
    note = "see https://docs.example.com/handlers for the full list"
)]
trait Handler<Req> {
    fn handle(&self, req: Req);
}

When to use it

This is a library-author tool. If you expose a public trait and expect users to implement it (or pass types that satisfy it), adding on_unimplemented is a small investment that saves your users real debugging time. Crates like bevy, axum, and diesel already use it to turn walls of trait errors into actionable guidance.

Stabilized in Rust 1.78, it’s part of the #[diagnostic] namespace — the compiler treats unrecognized diagnostic hints as soft warnings rather than hard errors, so it’s forward-compatible by design.

39. Trait Upcasting — Cast dyn Trait to dyn Supertrait

Since Rust 1.86, you can upcast a trait object to its supertrait — no workarounds needed.

The problem

Imagine you have a trait hierarchy:

1
2
3
4
5
use std::any::Any;

trait Animal: Any {
    fn name(&self) -> &str;
}

Before Rust 1.86, if you had a &dyn Animal, you couldn’t simply cast it to &dyn Any. You’d have to add an explicit method to your trait:

1
2
3
4
5
// The old workaround — adding a method just to upcast
trait Animal: Any {
    fn name(&self) -> &str;
    fn as_any(&self) -> &dyn Any;
}

This was boilerplate that every trait hierarchy had to carry around.

The fix: trait upcasting

Now you can coerce a trait object directly to any of its supertraits:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use std::any::Any;

trait Animal: Any {
    fn name(&self) -> &str;
}

struct Dog;

impl Animal for Dog {
    fn name(&self) -> &str {
        "Rex"
    }
}

fn print_if_dog(animal: &dyn Animal) {
    // Upcast to &dyn Any — just works!
    let any: &dyn Any = animal;

    if let Some(dog) = any.downcast_ref::<Dog>() {
        println!("Good boy, {}!", dog.name());
    } else {
        println!("Not a dog.");
    }
}

fn main() {
    let dog = Dog;
    print_if_dog(&dog);
}

The key line is let any: &dyn Any = animal; — this coercion from &dyn Animal to &dyn Any used to be a compiler error and now just works.

It works with any supertrait chain

Upcasting isn’t limited to Any. It works for any supertrait relationship:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
trait Drawable {
    fn draw(&self);
}

trait Widget: Drawable {
    fn click(&self);
}

fn draw_it(widget: &dyn Widget) {
    // Coerce &dyn Widget → &dyn Drawable
    let drawable: &dyn Drawable = widget;
    drawable.draw();
}

This makes trait object hierarchies much more ergonomic. No more as_drawable() helper methods cluttering your traits.