Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-475 · Case file with fixtures · Case 447 of 694 · Compiler evidence

A Private Rust Item Cannot Be Publicly Re-exported

A re-export can rename or relocate a public API but cannot widen a private item's visibility. Make the underlying item public only if it is a supported contract, or keep the façade restricted to the accessible scope.

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 re-export creates another accessible path but cannot widen the privacy of the item to which that path resolves.
First discriminating check
Decide whether the helper is supported API, then make the underlying item sufficiently public or restrict/remove the re-export and expose a safe wrapper.

I wrote pub use connect as public_connect inside a module while connect remained private. Rust emitted E0364 because a public re-export cannot promise access broader than the underlying item allows.

The failing fixture shows that re-exporting is not a visibility bypass. It creates another path to an item under the constraints of that item's privacy.

pub use combines aliasing and visibility

A normal use creates a local alias. pub use additionally makes the imported name available through the current module's public surface.

The E0364 explanation repairs the example by marking the underlying function pub. That gives the re-export a sufficiently visible item to expose.

The compiler prevents a façade path that downstream code could name but not legally reach.

Make the function public only as an API decision

The repaired fixture makes connect public and re-exports it as public_connect.

In a library, I first ask whether external callers should rely on the function's signature, behaviour, errors, and stability. Adding pub is not only a compiler fix; it expands the compatibility surface.

If the function is an implementation detail, I remove or restrict the re-export instead.

A private module can still hide the defining path

It is common to define a public item inside a private module and re-export the item at a public façade. The module path remains internal while the public item is reachable through its chosen public alias.

For example, mod implementation { pub struct Client; } with pub use implementation::Client; can expose crate::Client without exposing the module itself as a navigable API.

This works because the item is public even though the module containing its original path is private.

Restricted visibility must reach the destination

Rust supports pub(crate), pub(super), and pub(in path) visibility. A re-export cannot exceed the underlying restriction.

The visibility Reference describes public and restricted forms. I align the re-export with the narrowest scope that contains every intended caller.

For internal workspace architecture, pub(crate) may be the truthful boundary even if a convenient façade module reorganises paths.

Re-exports define canonical documentation paths

pub use can present a stable top-level API while implementation modules change. Documentation and generated links may treat the re-export as a primary route.

I choose one canonical public path and avoid exposing several equal routes accidentally. Too many aliases make search, examples, and semver review harder.

Deprecation attributes can guide users when replacing an established re-export.

Public signatures must not leak inaccessible types

Making connect public may reveal private parameter or return types, producing other privacy diagnostics or lints. I inspect the complete signature and trait bounds.

A public wrapper can translate internal types into stable domain types while keeping the implementation private. This is often better than recursively marking every dependency public.

Visibility should follow the intended abstraction, not the shortest path to a green build.

cfg-gated re-exports need aligned definitions

If a function exists only under one feature but its pub use is unconditional, other feature sets fail differently. I place compatible cfg conditions on the definition and export, or expose a stable cross-feature wrapper.

I compile the documented feature combinations because rust-analyzer may show only one active branch.

My E0364 checklist

I also check whether the exported function is safe to call without the internal module's surrounding assumptions. A private helper may expect pre-validation, lock ownership, or an initialised runtime supplied by sibling code. Marking it public because the signature happens to compile can expose an invalid call sequence. A public wrapper should establish those preconditions or return a controlled error. Visibility is not a safety proof, but changing it is a reason to review the proof boundary.

For unsafe functions, the public safety contract must document every caller obligation explicitly.

  • What is the underlying item's exact visibility?
  • How far does the re-export promise access?
  • Should this item really become supported public API?
  • Can a private module hide the implementation path while exporting a public item?
  • Would restricted visibility match the actual callers?
  • Does the public signature leak other private types?
  • Is there one intended canonical public path?
  • Are cfg conditions aligned across definition and re-export?

The core principle is that re-exporting reorganises accessible API; it does not manufacture accessibility. I decide the contract boundary first, then align the underlying item and façade path with that decision.