Skip to content
Select theme

How to fix error E0603: module is private

Short answer: the path you wrote is real — every name in it exists — but one of the modules along it is private, and the module you are writing in is not allowed to look inside it. The fix is to add visibility at the point the path crosses the wall: pub on the module, pub on the item inside it, or a pub use re-export that keeps the module private and publishes only the one path callers need. If the private module belongs to a crate you do not own, there is no pub you can add — the fix is to use the path the crate exports instead. The headline names which step of the path is closed — module \tcp` is private, function `connect` is private` — so read it as a map of the wall, not a generic complaint. Where E0432 means the path does not resolve at all, E0603 means it resolves and visibility stops it.

A module with a function in it, called from outside that module:

Compiling that with cargo build prints:

error[E0603]: module `tcp` is private
--> src/main.rs:10:25
|
10 | println!("{}", net::tcp::connect());
| ^^^ ------- function `connect` is not publicly re-exported
| |
| private module
|
note: the module `tcp` is defined here
--> src/main.rs:2:5
|
2 | mod tcp {
| ^^^^^^^
For more information about this error, try `rustc --explain E0603`.

Two things in this diagnostic are worth reading closely. First, the note at the bottom: tcp was declared without a pub, and in Rust that is not a style choice — it is the visibility. Every item is private by default, and “private” has a precise meaning: an item is visible in the module that declares it and in that module’s descendants, and nowhere else. tcp is declared inside net, so it is visible inside net and its children. main sits in the crate root, which is net’s parent — outside the wall — so net::tcp stops there.

Second, the double underline. The compiler underlines tcp and calls it a private module, but it also underlines connect and notes that the function is not publicly re-exported. It understood exactly what the path was reaching for — the connect function — and is reporting the first locked door on the way there. That is the whole model of this error: a path is a walk from module to module, and E0603 fires at the first step you are not allowed to take.

This prints:

connected

tcp got the pub; net did not — and the path still works, because visibility is judged one name at a time. main lives in the same module where net is declared, so reaching net itself is not crossing any wall; reaching into tcp is. In a real project the same question repeats at every level of the tree: mod net; in main.rs or lib.rs, mod tcp; inside net.rs, pub fn connect inside tcp.rs. The error’s note points at the line where the closed door was declared, so the fix usually goes on the line the note names.

E0603 does not only report modules. Once the module is public, the same error fires one level deeper, and the headline changes with it:

Compiling that with cargo build prints:

error[E0603]: function `connect` is private
--> src/main.rs:10:30
|
10 | println!("{}", net::tcp::connect());
| ^^^^^^^ private function
|
note: the function `connect` is defined here
--> src/main.rs:3:9
|
3 | fn connect() -> &'static str {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.

Now every module on the path is open and the function itself is not — so the headline says function \connect` is privateinstead ofmodule `tcp` is private. The fix is the same move at the new location: pub fn connect`. A path needs visibility at every name it crosses, and the diagnostic always names the first name that fails, which is why it is safe to just fix what the headline points at and recompile.

Don’t publish the module — re-export it

Section titled “Don’t publish the module — re-export it”

pub mod tcp opens the whole module: everything inside it becomes reachable from outside, forever, as net::tcp::whatever. Half the time that is more surface than you meant to hand out. The other fix keeps tcp private and publishes one path through it:

This prints:

connected

Callers now write net::connect, and net::tcp stays invisible — swap the whole tcp module for a different implementation and no caller notices. That is why so many crates keep their internal or platform modules private and re-export the useful names at the crate root: the re-export is the crate’s public API, and the module layout behind it is private.

The self:: in pub use self::tcp::connect; is not decoration. Inside a module, a use path must start with self::, super::, crate::, or a crate name — a bare tcp::connect does not resolve, and the compiler offers self:: as its suggestion (E0432 again, one level down).

Everything so far assumed the private module is in your own code, where adding a pub is yours to do. When E0603 fires on a path into a dependency, the wall is deliberate: the crate’s author decided that module is an implementation detail, and no amount of editing on your side changes that — your cargo build compiles the crate from crates.io, not your wishlist.

What to do instead, in order:

  • Check the crate root. Most crates re-export their public API one level deep or at the root: if some_crate::value::Value fails, some_crate::Value very often works. The docs.rs front page for the crate is the list of what is actually exported.
  • Search the crate’s docs for the item’s name. If it appears anywhere public, the docs show the path you are meant to use.
  • If it appears nowhere public, it is not part of the API. Reaching internals through #[doc(hidden)] paths or copied source couples you to a private implementation that can vanish in the next release; the durable fix is the documented way, or a different crate.

Plain pub is “visible to anyone who can reach the module”. Three narrower forms cover the in-between cases: pub(crate) makes the item visible anywhere in the current crate but no further, pub(super) makes it visible in the parent module, and pub(in path) restricts it to a named ancestor. They matter most in library code with private modules like the tcp example: a helper that is pub inside tcp but only needed by sibling modules in net can be pub(super) — reachable through the re-export, invisible to the crate’s users. If a pub feels too wide but private is too narrow, one of these is usually the answer.

  • unresolved import — E0432. That error means the path does not resolve: a name is misspelled, a module does not exist, or the use path needs a self::/crate:: prefix. E0603 means the path resolved fine and privacy stopped it. The tell is in the help blocks: E0432 talks about paths that do not exist, E0603 points at the line where the private item was declared.
  • undeclared crate or module — E0433. Same family, earlier in the walk: the very first segment of the path is not a crate or module the compiler knows — usually a dependency missing from Cargo.toml.
  • method is private — E0624. Calling a private method with dot syntax does not say module is private; it says error[E0624]: method `open` is private with a private method defined here note. Same wall, different syntax: paths through modules report E0603, dot-calls on methods report E0624.