Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-478 · Case file with fixtures · Case 450 of 694 · Compiler evidence

Rust Enum Variants Cannot Have Their Own Visibility Qualifier

Visibility belongs to the enum as a whole, not to individual variants. Remove the qualifier; if some states must remain unconstructible or hidden, redesign the public type boundary rather than annotating one variant.

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
Variants are components of the enum's single public contract and do not receive independent access qualifiers.
First discriminating check
Remove the qualifier and, if individual states need construction control, redesign around an opaque wrapper or private internal enum instead of per-variant visibility.

I wrote pub Ready inside pub enum State. Rust emitted E0449 because a variant cannot declare visibility independently from its enum.

The failing fixture demonstrates a design rule, not only forbidden punctuation: the set of variants is part of the enum type's visible shape.

Variants share the enum's visibility

If State is public and reachable, its variants are public under that same access boundary. If the enum is private or pub(crate), the variants follow it.

The E0449 explanation says visibility qualifiers are not permitted on enum variants, trait items, impl blocks, and extern blocks because those contexts inherit visibility from their parent contract.

The repaired fixture simply removes pub from Ready.

Public does not mean imported into scope

A public variant is reachable as State::Ready, but it does not become an unqualified local name automatically. Callers can qualify it or import it.

Visibility answers whether access is permitted; imports and paths answer how a name is resolved in a scope. Confusing these two concerns can lead to adding pub where a use or qualified path was needed.

I check the exact compiler diagnostic before changing API visibility.

Individual hidden variants need a different model

Rust does not support a public enum with one private variant. If external callers must observe a state but must not construct certain representations, a public opaque struct with query methods and private internal enum can be more suitable.

Another option is a public enum marked #[non_exhaustive], which affects downstream construction and exhaustive matching, but it does not make named existing variants private.

I select the model from what callers may construct, match, and rely on over time.

Fields of enum variants also share the enum's visibility policy rather than accepting independent pub qualifiers. A public data-carrying variant exposes its field positions or names through construction and patterns.

If payload internals need privacy, I wrap them in a public type with private fields and controlled constructors. The variant can carry that opaque type without exposing its representation.

This preserves the enum state vocabulary while protecting invariants.

Trait items form one contract too

Methods inside a public trait do not receive individual pub qualifiers. Implementors and users see them according to the trait's visibility. Similarly, methods implementing that trait cannot add pub in the trait impl block.

Inherent impl methods are different: they may declare their own visibility. I identify whether a block is inherent or implements a trait before applying a visibility fix.

The same E0449 code can therefore point to several contexts with one parent-visibility principle.

Public enum evolution needs planning

Adding a variant to a public exhaustive enum can break downstream matches. #[non_exhaustive] is a forward-compatibility decision that requires downstream wildcard handling.

Removing pub from a variant syntax error does not settle this API concern. I review whether external exhaustive matching is an intended promise before releasing the enum.

The enum Reference defines the variant forms; the visibility Reference defines access paths.

My E0449 checklist

I also distinguish construction control from validation. Even when all variants are public, a system can validate an enum at the boundary before accepting it into a stronger wrapper type. This is useful for deserialized values, because hiding constructors alone would not stop an external data format from containing unexpected states. A validated newtype can separate “syntactically representable” from “accepted by this subsystem,” while the public enum remains useful for interchange.

That design is heavier than removing one pub, so I use it only when the invariant matters.

  • Which parent item already controls visibility?
  • Is this an enum variant, trait item, trait impl, or extern block?
  • Did I actually need a qualified path or import instead of pub?
  • Must callers construct and exhaustively match every visible state?
  • Would an opaque public wrapper better protect hidden states?
  • Should payload data be wrapped behind private fields?
  • Is this an inherent impl where item visibility is permitted?
  • Does enum evolution require non_exhaustive planning?

The core principle is that some Rust items are components of one enclosing contract. Their accessibility follows that contract as a unit. When I need finer control, I redesign the type boundary instead of attaching unsupported visibility to one component.