Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-551 · Case file with fixtures · Case 523 of 694 · Compiler evidence

A Public Rust Associated Type Cannot Reveal a Private Type

Visibility must be coherent across an interface. Either expose the associated type, reduce the trait's reach, or hide representation behind public behaviour.

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
The selected associated type has narrower visibility than the public type-level contract through which it is exposed.
First discriminating check
Publish a stable wrapper or type when it is real API, narrow the trait's visibility for internal plumbing, or redesign operations to hide representation.

A public trait implementation becomes part of what downstream code can reason about. If its associated type is private, the interface offers a type-level answer that users cannot legally access. Rust reports E0446.

The failing fixture implements public Decode for u64 and selects private InternalRecord as Record.

Associated types are visible through projections

Users can refer to an implementation result as <u64 as Decode>::Record. Generic functions may return it, constrain it, or call trait operations that mention it. The concrete selected type is therefore part of the public contract even if no method names it directly in the fixture.

The associated types Reference explains how an implementation supplies the concrete type behind a trait's associated item.

E0446 protects the usability of that projection. A public path cannot promise a result whose visibility ends earlier.

Make the type public when it is genuinely API

The repaired fixture publishes PublicRecord and its example field. Downstream code can now name and use the projected type.

Making a type public is a compatibility commitment. Its name and exposed fields constrain future changes. In a library, I consider a constructor and private fields so the type may evolve without allowing arbitrary invalid construction.

The repair is correct only when the record itself belongs in the supported API.

Narrow the trait when the implementation is internal

The official E0446 page gives another repair: reduce the trait's visibility, for example to pub(crate). Then all users of the interface live inside a scope that can also access the internal type.

This is often better for implementation plumbing. Not every abstraction used across modules needs to become a downstream extension point.

I align the visibility of the trait, implementation surface, and concrete associated types with the audience that should rely on them.

Hide representation behind public operations

If callers need decoded behaviour but should not know the record representation, I can redesign the public trait. It might return a public view, iterator, owned DTO, or opaque impl Trait from a suitable function boundary.

Another option is a public wrapper with private fields. The wrapper becomes the stable associated type while internal storage remains changeable.

This is more deliberate than simply adding pub until compilation succeeds. Privacy errors often reveal an accidental architecture leak.

Public does not mean every field must be public

Publishing struct Record { hidden: Internal } can be valid when the field remains private and public operations avoid exposing its type. Users can own and pass the record without constructing or destructuring private internals.

The Reference's visibility and privacy chapter describes which items are externally accessible and how pub(crate) and restricted visibility work.

I test the API from an integration test or small dependent crate because code inside the defining module has extra access and can hide visibility mistakes.

A library may expose a trait for bounds while preventing downstream implementations through a private supertrait. That pattern should be intentional and documented because users can call the trait but cannot implement it.

E0446 is different: it concerns a concrete private item leaking through a public interface. Still, both cases require deciding who may name, call, and implement each part of a contract.

Documentation should show the downstream view

I check generated documentation as if I were a user in another crate. If a return type expands into an inaccessible path, even clever bounds can leave the API impossible to explain. A public wrapper with documented methods usually produces a better contract than an exposed internal storage record. I also add a compile test that imports only public paths. This catches accidental visibility leaks that unit tests inside the defining module cannot see and gives future refactors an executable statement of the supported surface.

My E0446 checklist

  • Which public trait or implementation exposes the projection?
  • What is the visibility of the selected associated type?
  • Should downstream users name or construct that type?
  • Would pub(crate) better match an internal abstraction?
  • Can a public wrapper keep fields and representation private?
  • Does a method return another private type indirectly?
  • Have I tested access from outside the defining module or crate?
  • What compatibility commitment does adding pub create?

The core principle is that public interfaces must be usable from their promised scope. I expose a stable type, narrow the whole contract, or redesign the boundary so private representation stays truly private.