Supertraits in Rust and Building Trait Hierarchies Safely
A supertrait says a trait requires another one. See the syntax, how methods are inherited, trait objects with supertraits and upcasting.

A supertrait is a trait that another trait requires. Writing trait Pet: Named + Debug says that nothing can be a Pet unless it is also Named and Debug. Rust has no inheritance of data, so supertraits are the nearest thing to it: they build a hierarchy of capabilities, where a more specific trait promises everything its parents promise. The payoff is that generic code needs only one bound, P: Pet, and gets all the methods of Pet, Named and Debug. This post builds a small hierarchy, shows the default methods that become possible, the compiler error when a type implements the child and forgets a parent, and the interaction with trait objects, including upcasting. The code compiled and ran on Rust 1.99, and the outputs are the programs' own.
The syntax and what it means
use std::fmt::{self, Debug, Display};
trait Named {
fn name(&self) -> String;
}
// Pet requires Named and Debug: any Pet is also Named and Debug.
trait Pet: Named + Debug {
fn sound(&self) -> String;
// A default method can use the supertrait's methods.
fn introduce(&self) -> String {
format!("{} says {} ({:?})", self.name(), self.sound(), self)
}
}
#[derive(Debug)]
struct Dog {
name: String,
}
impl Named for Dog {
fn name(&self) -> String {
self.name.clone()
}
}
impl Pet for Dog {
fn sound(&self) -> String {
"woof".into()
}
}
// Generic code needs only the subtrait bound; the supertraits come with it.
fn announce<P: Pet>(pet: &P) -> String {
format!("{} / debug: {:?}", pet.name(), pet)
}
// Upcasting a trait object to its supertrait (stable since Rust 1.86).
fn just_the_name(pet: &dyn Pet) -> String {
let named: &dyn Named = pet;
named.name()
}
// Display as a supertrait: the trait promises printable values.
trait Report: Display {
fn headline(&self) -> String {
format!("== {self} ==")
}
}
struct Outage(u32);
impl Display for Outage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "outage #{}", self.0)
}
}
impl Report for Outage {}
fn main() {
let d = Dog { name: "Rex".into() };
println!("{}", d.introduce());
println!("{}", announce(&d));
println!("{}", just_the_name(&d));
println!("{}", Outage(7).headline());
}Rex says woof (Dog { name: "Rex" })
Rex / debug: Dog { name: "Rex" }
Rex
== outage #7 ==trait Pet: Named + Debug declares two supertraits. It does not mean Pet contains the methods of Named; it means that any type implementing Pet must also implement Named and Debug, and the compiler enforces that when you write impl Pet for Dog. The consequence shows in the default method introduce, which calls self.name() from Named and prints self with {:?} from Debug. A default method inside Pet may use anything its supertraits provide, because the bound guarantees it. Without the supertrait declaration, introduce would not compile, since Self is otherwise opaque.
The same holds from the outside. announce<P: Pet>(pet: &P) uses pet.name() and {:?} with only the bound P: Pet. The caller of a generic function does not have to write P: Pet + Named + Debug. This is the main design benefit: the hierarchy gives a single name to a bundle of capabilities.
Display as a supertrait
The last part of the program shows the pattern the standard library favors. trait Report: Display promises that every report can be printed, so a default method can interpolate self directly: format!("== {self} =="). Outage implements Display by hand, then adds impl Report for Outage {} with an empty body to inherit headline, giving == outage #7 ==. Using Display, Debug, Send and Sync as supertraits is common and safe. The std::error::Error trait is the best known example: it requires Debug + Display, which is why every error type can be printed in both forms.
The error when a parent is missing
Implementing the child without the parent is rejected at the impl line, and the message names both missing pieces:
trait Pet: Named + Debug {
fn sound(&self) -> String;
}
struct Dog;
impl Pet for Dog {
fn sound(&self) -> String {
"woof".into()
}
}error[E0277]: `Dog` doesn't implement `Debug`
--> t172x.rs:13:14
|
13 | impl Pet for Dog {
| ^^^ the trait `Debug` is not implemented for `Dog`
|
note: required by a bound in `Pet`
--> t172x.rs:7:20
|
7 | trait Pet: Named + Debug {
| ^^^^^ required by this bound in `Pet`
help: consider annotating `Dog` with `#[derive(Debug)]`
error[E0277]: the trait bound `Dog: Named` is not satisfied
--> t172x.rs:13:14
|
13 | impl Pet for Dog {
| ^^^ unsatisfied trait boundThe compiler reports each missing supertrait as a separate E0277, points at the line in the child trait that demanded it, and, where it can, suggests the fix: a #[derive(Debug)], or an impl Named for Dog. Implement the supertraits first, then the child. The error appears at the implementation and not at a distant call, which is what makes supertraits easy to debug.
Supertraits and trait objects
Supertraits carry their methods into trait objects. A &dyn Pet can call name() and format with {:?} because Pet guarantees them. Since Rust 1.86, a &dyn Pet can also be converted to &dyn Named, an upcast, as just_the_name does: let named: &dyn Named = pet;. Before that release you needed a helper method like fn as_named(&self) -> &dyn Named in the trait. Upcasting follows the hierarchy only upward, from child to parent; you cannot downcast from &dyn Named back to &dyn Pet without Any.
The catch is the dyn compatibility of the supertraits. A supertrait that is not dyn compatible makes the child trait not dyn compatible. The classic case is Clone: trait Solid: Shape + Clone cannot be used as Box<dyn Solid>, because Clone requires Sized and returns Self. The compiler's reply to Vec<Box<dyn Solid>> is E0038, with the reason "it requires Self: Sized", which is the Clone requirement surfacing through Solid. If you need cloning, use the clone_box pattern from dyn compatibility, and keep supertraits to those that are dyn compatible: Debug, Display, Send, Sync, Any.
Here is the real message for that case:
error[E0038]: the trait `Solid` is not dyn compatible
--> t172y.rs:23:28
|
23 | let items: Vec<Box<dyn Solid>> = vec![Box::new(Square(2.0))];
| ^^^^^ `Solid` is not dyn compatible
|
note: for a trait to be dyn compatible it needs to allow building a vtable
5 | trait Solid: Shape + Clone {
| ----- ^^^^^ ...because it requires `Self: Sized`
| |
| this trait is not dyn compatible...The ...because line points at Clone in the supertrait list, not at any method of Solid. That is the signature of a bad supertrait: look at the list after the colon first.
When to use supertraits and when to bound the function instead
A supertrait is a statement about the trait itself: every implementer is also the parent. A bound on a function is a statement about that function's needs. The question to ask is whether the capability is intrinsic. A Pet is always Named, so a supertrait makes sense. A function that happens to print its argument needs T: Debug as a bound, and putting Debug on an unrelated trait would force every implementer to derive it for no reason. Supertraits are a commitment: adding one later breaks all existing implementers who lack it.
Two further patterns use supertraits well. A blanket helper trait names a combination of bounds: trait Number: Copy + Add<Output = Self> + PartialOrd {} plus impl<T: Copy + Add<Output = T> + PartialOrd> Number for T {} lets you write T: Number everywhere. And extension traits layer on top of a base: trait ReadExt: Read adds convenience methods to everything readable. The base trait guarantees read, and the extension trait implements its methods in terms of it, as in the Logger2: Logger example in default methods.
What supertraits do not do
They do not give you shared fields or implementations. Dog still implements Named itself; Pet only checks that it did. They do not create subtyping in the sense of other languages: a Vec<Dog> is not a Vec<dyn Pet>, and generic code sees no relationship between Pet and Named beyond the bound. And they are not a place to hide a large interface. A trait with five supertraits and ten methods is a sign to split it. For where supertraits sit among blanket implementations and the orphan rule, read blanket implementations and traits explained.


