How to Use #[doc(hidden)] to Hide Items from Documentation

Use the #[doc(hidden)] attribute above an item to hide it from generated Rust documentation while keeping it accessible in code.

When the compiler needs pub but the docs need silence

You are building a library crate. You have a helper function that performs a complex calculation. Another crate in your workspace needs to call this function. You make it pub. Now your documentation renders the helper alongside your clean public API. Users see calculate_hash_internal_v2 and wonder if they should use it. You don't want that noise. You also can't make it pub(crate) because the other crate won't see it. You need a way to keep the item public for compilation while removing it from the published documentation.

That is what #[doc(hidden)] does. It tells rustdoc to skip an item while leaving the item fully public to the compiler. The attribute separates documentation visibility from code visibility. Code visibility controls what the compiler allows other crates to access. Documentation visibility controls what rustdoc renders in the HTML output. They are independent axes.

How the attribute works

#[doc(hidden)] is an attribute applied to any item that can be documented: functions, structs, traits, modules, constants, and macros. When rustdoc processes the crate, it checks for this attribute. If present, rustdoc excludes the item from the generated HTML, the sidebar, and the search index. The item remains part of the crate's public interface. Downstream crates can import and use it without errors.

The compiler ignores #[doc(hidden)]. The compiler only cares about visibility modifiers like pub, pub(crate), and pub(super). If an item is pub, the compiler treats it as public regardless of documentation attributes. This distinction is the source of most confusion. The attribute hides the item from the reader, not from the linker.

/// Formats the internal error code for logging.
/// This function is public because the `workspace-logger` crate
/// depends on it, but it is not part of the stable API.
#[doc(hidden)]
pub fn format_error_code(code: u32) -> String {
    // Returns a hex string like "ERR-00A1".
    // The format is an implementation detail, but the signature
    // must remain stable for downstream crates.
    format!("ERR-{:04X}", code)
}

The function is public. Any crate can call my_crate::format_error_code(42). The documentation generator simply refuses to show it.

The semver trap

#[doc(hidden)] does not grant semver immunity. This is the most important rule. The attribute hides the item from the documentation, but the item remains public API. If you change the signature, remove the item, or change its behavior in a breaking way, you must bump the major version.

New maintainers often hide internal helpers and then change them freely, assuming that because the items are hidden, they are exempt from semver. They are not. If a downstream user finds the hidden item through an IDE, by reading the source code, or by guessing the name, they can use it. If you break that usage, you break their build. The semver contract applies to all public items, hidden or not.

Treat #[doc(hidden)] items as public API. Breaking them is a semver violation, even if nobody can see them.

Realistic use case: Macro support items

Macros often require helper functions or types that must be public. When a macro expands, it generates code that runs in the caller's crate. If the macro references a function in the defining crate, that function must be public. Macros cannot call private items across crate boundaries.

You don't want users calling the helper directly. The helper is an implementation detail of the macro. #[doc(hidden)] is the standard solution.

// In lib.rs

/// Internal helper for the `my_debug!` macro.
/// This function captures the debug representation of an expression.
/// It is public so the macro can reference it, but it is hidden
/// from documentation to discourage direct usage.
#[doc(hidden)]
pub mod internal {
    /// Captures the debug string of an expression.
    /// Do not call this function directly.
    pub fn capture(expr: &dyn std::fmt::Debug) -> String {
        // Returns the debug string for logging.
        // The function signature is stable because the macro depends on it.
        format!("{:?}", expr)
    }
}

/// A debug macro that prints the expression and its value.
/// Expands to a call to `internal::capture`.
#[macro_export]
macro_rules! my_debug {
    ($e:expr) => {
        // The macro expands to code that calls the hidden helper.
        // The helper must be public for this to compile in downstream crates.
        println!("DEBUG: {} = {}", stringify!($e), $crate::internal::capture(&$e));
    };
}

The module internal is public. The function capture is public. The macro references $crate::internal::capture. This compiles correctly. The documentation for the crate shows the macro but hides the module and function. Users see a clean API. The macro works.

Realistic use case: Workspace sharing

You have a workspace with multiple crates. my-lib provides core functionality. my-app is the binary crate. my-app needs to access a function in my-lib that is not intended for general consumers. You could make my-lib a dev-dependency of my-app, but that doesn't work if my-app depends on my-lib for other reasons. You could use pub(crate) in my-lib, but my-app won't see it.

You make the function pub and add #[doc(hidden)]. This allows my-app to use the function while keeping the documentation clean for external users.

// In my-lib/src/lib.rs

/// Checks if the database connection is healthy.
/// This function is public because `my-app` uses it for startup checks,
/// but it is hidden from the public API documentation.
#[doc(hidden)]
pub fn check_db_health() -> bool {
    // Returns true if the connection is active.
    // The implementation may change, but the signature is stable
    // for the workspace binary.
    true
}

my-app can call my_lib::check_db_health(). External crates can also call it, but they won't see it in the docs. The convention here is to accept that the item is public and manage the risk through naming or documentation warnings.

Pitfalls and conventions

IDEs still show hidden items. rust-analyzer and other tools index the public API. They do not always respect #[doc(hidden)]. Users may see the item in autocomplete. They may use it. You cannot prevent this. The attribute only affects rustdoc.

Naming conventions help discourage usage. The community often prefixes hidden items with __ or _internal. This signals that the item is not for general use. You can also add a warning in the doc comment.

/// WARNING: This function is internal to the macro system.
/// Do not use this function directly. It may change without notice.
#[doc(hidden)]
pub fn __macro_helper_impl() {
    // Implementation...
}

The standard library uses #[doc(hidden)] for unstable features. Unstable traits and functions are often exposed via feature flags but hidden from documentation until they are stabilized. This signals that the item exists but is not ready for public use.

Convention aside: pub(crate) is usually what you want. #[doc(hidden)] is a sledgehammer for specific cases where the compiler demands pub but the docs demand silence. Don't use #[doc(hidden)] to hide pub(crate) items. If an item is pub(crate), it is already hidden from documentation and restricted to the crate. Adding #[doc(hidden)] is redundant.

Decision matrix

Use pub(crate) when the item should only be accessible within the current crate. This restricts code access and hides the item from documentation automatically. This is the default choice for internal helpers.

Use #[doc(hidden)] when the item must be public for compilation reasons, such as macro support, workspace sharing, FFI bindings, or unstable features, but should not appear in the published documentation.

Use pub(super) when the item is only needed by the parent module. This provides precise visibility control within a module tree.

Use pub(in path) when you need to restrict visibility to a specific module path. This is useful for deep module hierarchies where you want to share an item with a sibling or ancestor module but not the whole crate.

Reach for pub(crate) first. #[doc(hidden)] is for when the compiler demands pub but the docs demand silence.

Where to go next