Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-476 · Case file with fixtures · Case 448 of 694 · Compiler evidence

A Private Rust Module Cannot Be Publicly Re-exported as a Module

Re-exporting a module exposes its namespace as API, so the source module needs compatible visibility. Prefer selective item re-exports when callers should not depend on the internal module layout.

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
Re-exporting the module exposes its namespace topology, but the source module's visibility does not permit that external path.
First discriminating check
Choose whether module layout is public API; make it public when intentional or selectively re-export only supported public items through the façade.

I declared a private engine module and wrote pub use engine as public_engine. Rust emitted E0365 because the module itself is only crate-visible and cannot be re-exported as an externally public module.

The failing fixture differs from re-exporting one public item out of a private implementation module. Here the export attempts to expose the module namespace itself.

A module alias exposes a namespace

Downstream callers of a successful re-export could write public_engine::VERSION and browse other public items in that module. The module therefore becomes part of the public path structure.

The E0365 explanation repairs the example by declaring pub mod engine. Its public alias now has an underlying module with compatible visibility.

Rust will not let an alias widen the source module beyond its declaration.

Expose the module only when its layout is API

The repaired fixture makes engine public and retains the alias. That is appropriate only if callers may depend on the module boundary and its public contents.

Public module layout affects documentation paths, imports, and semver. Moving an item out later can require a compatibility re-export.

I avoid publishing internal architecture accidentally to make one constant reachable.

Selective item re-exports preserve encapsulation

If callers need only VERSION or Client, I can keep the module private, mark those items pub, and re-export the items individually at the parent:

mod engine {
    pub struct Client;
}

pub use engine::Client;

The defining path stays internal while Client receives one chosen public route. This often creates a cleaner façade and leaves implementation files free to move.

Module visibility and item visibility are separate

A public item inside a private module is not reachable through the private module from outside, but it may be re-exported through a public ancestor. Conversely, making the module public does not automatically make all its private contents public.

The visibility Reference applies access along every path segment. I inspect the whole route rather than only the final item.

This explains many “it says pub already” surprises.

Aliases can support migrations

A library may rename engine to runtime and temporarily pub use runtime as engine for compatibility. Both module visibilities and deprecation strategy need care.

I choose one canonical documented path and keep compatibility aliases intentional. Permanent duplicate namespaces increase discoverability noise and allow users to mix equivalent types from confusing paths.

When feasible, item-level aliases make a migration more targeted.

Restricted modules serve internal architecture

pub(crate) or pub(super) modules can organise a crate without becoming external API. Re-exporting them at the same restricted visibility can be useful for internal façades.

I match the export qualifier to the actual consumer set. Not every shared workspace component needs pub to the world.

The module Reference explains how modules form the crate's tree and paths.

Feature-specific modules need a stable surface

Backends often live behind feature flags. Publicly re-exporting whichever backend module is active can make the API shape vary between builds.

I prefer a stable public trait or façade type, with feature-selected private modules providing implementations. When backend-specific APIs are intentionally public, documentation must state their cfg conditions and exports must mirror them.

My E0365 checklist

Documentation tests are a useful check for the chosen topology. I write one example importing the type through the intended public façade and avoid examples using the private implementation path. If rustdoc displays both paths, I inspect inline and no-inline re-export attributes only after the semantic visibility is correct. Presentation controls can improve docs, but they cannot repair an inaccessible source module or substitute for a coherent public ownership decision.

The final API should remain understandable without knowing how source files are arranged.

  • Am I re-exporting one item or the module namespace itself?
  • What is the source module's exact visibility?
  • Should downstream callers depend on this module boundary?
  • Would selective item re-exports create a smaller stable façade?
  • Are all path segments accessible at the promised scope?
  • Is this alias temporary compatibility or permanent API?
  • Could restricted visibility satisfy internal consumers?
  • Do features make the public module surface inconsistent?

The core principle is that modules are part of Rust's visible API topology. I expose a module only when that namespace is a supported contract; otherwise I re-export the smallest public items through a stable façade.