How to Use the pub(crate) and pub(super) Visibility Modifiers

pub(crate) and pub(super) let you scope visibility to your crate or to the parent module, keeping internal helpers off the public API. Walk through real examples and E0603 fixes.

When pub is too much

You split your project into modules. You wrote a helper function in mod parsing that the rest of your crate calls. You marked it pub fn parse_header(...) because the function in mod http next door needs it. Everything compiles. Then you publish your library and realise that parse_header is now part of your public API. Anyone using your crate can call it. You didn't mean for that. You meant "this is shared inside my crate" not "this is shared with the world."

This is exactly the gap that pub(crate) and pub(super) fill. They let you say how far an item is visible, not just public-or-not. Once you understand them, your modules feel less like a wall with one door and more like a building with rooms, hallways, and a lobby.

What "visibility" actually means in Rust

Every item in Rust (a function, struct, module, constant, anything you can name) has a visibility. The visibility decides who is allowed to refer to that item by its path.

The defaults are stricter than most languages. Without any modifier, an item is private to its own module. Even a function in a child module can't see it without permission. To open the door wider you write pub, optionally with a scope:

  • pub means visible everywhere the parent path is visible. In a library crate, that effectively means "visible to your users."
  • pub(crate) means visible anywhere inside the current crate, but not to the outside world.
  • pub(super) means visible to the parent module and its descendants.
  • pub(in some::path) means visible inside the named module path. Mostly used in macro-heavy code.
  • pub(self) is the same as no pub at all (rarely written).

The one to internalise first is pub(crate). It's the workhorse for organizing internal helpers in any non-trivial crate.

A minimal example

// src/lib.rs (the crate root)

mod parsing {
    // Visible to anything in this crate, but not to users of the library.
    pub(crate) fn parse_header(input: &str) -> &str {
        input.trim_start()
    }

    // Visible only inside the parsing module itself. The default.
    fn helper() {}
}

mod http {
    pub fn handle_request(line: &str) -> &str {
        // We can call parse_header because it's pub(crate).
        crate::parsing::parse_header(line)
    }
}

// Users of our library can call this.
pub use http::handle_request;

If we'd written pub fn parse_header instead of pub(crate), our users would be able to call mycrate::parsing::parse_header directly, which means we couldn't change its signature, rename it, or delete it without a major version bump. By marking it pub(crate), we keep the door open inside our crate and closed to the outside.

When pub(super) shines

pub(super) is narrower than pub(crate). It says "my parent module can see this; nobody further out can." That's useful when a child module exposes a hook that's only meant for its sibling modules to use, not for the whole crate.

mod database {
    // Anyone in our crate is allowed to query.
    pub(crate) fn query(sql: &str) {
        let conn = connection::open();
        // ... do the work
    }

    mod connection {
        // Only the database module (parent) can open a connection.
        // Other parts of the crate must go through query().
        pub(super) fn open() -> Conn {
            // ...
            # Conn
        }

        # pub(super) struct Conn;
    }
}

The goal here is to keep the connection module's surface tight. Without pub(super), you'd have to pick between pub(crate) (too wide, exposes connection management to every module) or no pub (too narrow, the parent can't even use it).

A more realistic example: a crate with a public facade

Many libraries follow a pattern where the crate root exports a small, polished API and the implementation lives in deeper modules. Visibility modifiers are how you keep the implementation modules from leaking.

// src/lib.rs
mod tokenizer;        // private module, not even pub
mod parser;           // private module
mod ast;              // private module

// The only thing users see.
pub use parser::Parser;
pub use ast::Tree;

// src/tokenizer.rs
pub(crate) struct Token {        // crate-internal type
    pub(crate) kind: TokenKind,
    pub(crate) lexeme: String,
}

// Visible inside the tokenizer module and to its parent (lib.rs).
// Other modules in the crate go through Token instead of TokenKind directly.
pub(super) enum TokenKind {
    Ident,
    Number,
    Op,
}

// src/parser.rs
use crate::tokenizer::Token;       // works because Token is pub(crate)

pub struct Parser {
    // The Vec<Token> is internal state. The fields are not pub, so users can't poke at it.
    tokens: Vec<Token>,
}

impl Parser {
    pub fn new(input: &str) -> Self { /* ... */ # Self { tokens: Vec::new() }
 }
    pub fn parse(&self) -> crate::ast::Tree { /* ... */ # crate::ast::Tree
 }
}

The big idea: pub is for the few items users should know about. pub(crate) is for everything internal that crosses module boundaries. pub(super) is for items that only the immediate parent should see.

What the compiler will tell you when you get it wrong

If you try to use a privately-scoped item from outside its visibility, you'll see something like:

error[E0603]: function `helper` is private
  --> src/main.rs:5:23
   |
5  |     parsing::helper();
   |              ^^^^^^ private function
   |
note: the function `helper` is defined here
  --> src/parsing.rs:8:5
   |
8  | fn helper() {}
   |     ^^^^^^

E0603 is the visibility error. The compiler will tell you exactly where the item is defined and how it's declared. The fix is usually one of:

  • Decide the item should be more visible and widen its modifier (pub(crate)).
  • Decide the caller shouldn't be reaching for that item directly and refactor instead.

Don't reflexively widen visibility. The narrower it is, the more freedom you have to refactor without breaking callers.

A quick guide to picking the right modifier

Walking from least to most visible:

No modifier: this item is an implementation detail of one module. Nothing outside that module needs it.

pub(super): this item is shared with the immediate parent. Sibling modules of the parent get it for free, but cousins further out don't.

pub(crate): this item crosses module boundaries inside the crate. It's the right pick for shared helpers, internal types passed between modules, and most things you'd be tempted to mark pub.

pub: this item is part of the crate's public API. Once it ships, breaking it is a semver-major change. Reserve this for items you're committed to supporting.

pub(in some::path): rarely needed, but useful when you want to expose something only inside a specific subsystem (pub(in crate::compiler)).

Common pitfalls

Over-using pub. Tutorials often mark everything pub to keep examples short. In a real crate that's a maintenance trap: every pub item is a promise.

Forgetting that pub use re-exports change visibility. If you write pub use crate::parser::internal::Helper; in lib.rs, you've made Helper part of your public API even though it lives in a private module.

Assuming pub on a struct means its fields are public. It doesn't. Fields have their own visibility. pub struct Foo { pub a: i32, b: i32 } exposes a and keeps b private.

Using pub(super) from the crate root. pub(super) at the root level is the same as pub(crate) because the root has no parent above the crate. The compiler will accept it but readers find it confusing. Use pub(crate) instead.

Relying on default visibility for tests. Functions you want to call from #[cfg(test)] modules need to be visible to those modules. Either mark them pub(crate) or add #[cfg(test)] pub(crate) fn ... to expose only in tests.

When to reach for what

Default to no pub modifier. Add visibility only when something else needs to call it.

When the caller is inside the same crate, almost always pub(crate).

Reach for pub(super) when only the parent module is the legitimate caller. Common in deeply nested module trees where each layer wraps the layer below.

Reach for pub when you genuinely want a stable, documented API. Each pub item should be one you'd be willing to defend in the changelog if you removed it.

Where to go next

How to Use pub to Control Visibility in Rust

How to Organize a Large Rust Project into Modules

How to Create an Internal (Private) Module in Rust