Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-536 · Case file with fixtures · Case 508 of 694 · Compiler evidence

A Rust Trait Object Already Satisfies Its Supertraits

A supertrait is part of the subtrait contract. A dyn subtrait does not need, and cannot receive, a duplicate implementation of that inherited requirement.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
A subtrait includes its supertrait contract, so the dyn subtrait cannot receive a competing duplicate implementation.
First discriminating check
Implement both required contracts on the concrete type, use inherited methods through dyn Service, and introduce a wrapper for genuinely different erased behaviour.

When I first met a dyn Service, it was tempting to treat it as a new concrete type that needed all of its own implementations. E0371 shows an important boundary: a trait object for a subtrait already satisfies the supertraits named by that subtrait.

The failing fixture declares trait Service: Observable and then tries impl Observable for dyn Service. Rust rejects the duplicate because Observable is already part of what being a Service means.

A supertrait is a required capability

The colon in trait Service: Observable says every implementor of Service must also implement Observable. It does not copy method bodies into each implementation, and it is not optional inheritance.

Code generic over T: Service may rely on Observable operations. A dyn Service likewise carries a vtable supporting the dyn-compatible operations promised through that relationship.

The Reference calls out this implication in its supertraits section. The official E0371 explanation says implementing the supertrait again is not useful and is forbidden.

Implement contracts on the concrete type

The repaired fixture introduces Api. It implements Observable and then Service. A borrowed &dyn Service can call label() without any implementation on the trait-object type.

This is the direction I normally want:

Api -> Observable
Api -> Service, which requires Observable
&Api -> coerced to &dyn Service

The concrete implementation supplies behaviour. The trait object erases the concrete identity while preserving the selected interface.

The trait object is not a place to override inherited behaviour

Suppose I want every dynamically dispatched service to use a different label from statically dispatched services. A blanket-looking impl Observable for dyn Service might seem attractive, but it would create two competing answers for the same underlying contract.

Instead, I put behaviour in each concrete implementation, provide a default method on the trait, or introduce a wrapper with its own explicit semantics. A wrapper is a new concrete type and can safely define how it delegates or changes behaviour.

This keeps dispatch coherent: the same service does not silently change its Observable identity merely because a caller erased its concrete type.

Trait objects contain a pointer and dispatch metadata

A trait object such as &dyn Service is a dynamically sized abstraction accessed behind a pointer. Conceptually, it carries a data pointer plus metadata used for method dispatch. The trait object Reference describes the supported object form and its bounds.

It is not a class instance constructed from the trait declaration. The underlying value is still Api or another concrete implementor. This mental model prevents me from searching for a constructor or a second implementation “for the dyn type.”

The fact that Service: Observable permits using observable methods through a service object. Converting one trait-object pointer form to another has its own language rules and version history. I verify current compiler support rather than assuming every generic implication is also an automatic pointer coercion in every position.

For many APIs, I do not need an explicit upcast at all. A function accepting &dyn Service can call the supertrait method directly, as the fixture demonstrates.

Keeping “method availability” separate from “changing the trait object type” makes diagnostics much easier to interpret.

Cycles in supertraits describe an impossible hierarchy

If Service: Observable and Observable: Service, neither contract is foundational. Rust detects a dependency cycle rather than building an infinitely recursive promise. Shared capabilities belong in a third base trait, or the two traits should be independent bounds at the use site.

That is different from E0371, but both failures encourage me to draw the trait graph. Arrows should express genuine prerequisite capabilities, and the graph should have a sensible direction.

My E0371 checklist

  • Does the target trait already name the other trait as a supertrait?
  • Is the implementation being written for a concrete type or dyn Trait?
  • Which concrete type should supply the actual method behaviour?
  • Can a default supertrait method express shared behaviour?
  • Would a wrapper model intentional dynamic-only behaviour more honestly?
  • Am I confusing availability of methods with trait-object upcasting?
  • Does the supertrait graph have a clear acyclic direction?
  • Can a small call through &dyn Subtrait prove the method is already available?

The core principle is that a subtrait includes its supertrait obligations. I implement those obligations on concrete types and let the trait object preserve them, instead of trying to define the same relationship twice.