When the code works but the memory fades
You finish a crate. It compiles. The tests pass. You push it to GitHub and feel that warm glow of completion. Three weeks later, you open a new project and decide to use your own library. You grep for the function name. You find the signature. You stare at pub fn process(data: &[u8], flags: u32) -> Result<Output, Error> and realize you have no idea what flags actually controls. You spend forty minutes tracing the source code to figure out that bit 3 enables verbose logging and bit 7 triggers a cache flush.
Or worse, a user opens an issue. "How do I use this?" they ask. You reply with a vague explanation. They reply with a screenshot of the generated docs, which are empty.
Rust treats documentation as a first-class citizen, not an afterthought. The cargo doc command isn't a separate tool you install. It's wired directly into the compiler toolchain. It parses your comments, generates a searchable HTML website, and runs the code examples you write. If your documentation contains broken code, the build fails. The docs are code. Treat them that way.
How rustdoc turns comments into a website
Rust ships with a tool called rustdoc. When you run cargo doc, Cargo invokes rustdoc to scan your crate. It looks for special comment markers. It extracts the text, the type signatures, and the code blocks. It builds a static site in the target/doc directory.
There are two comment styles that matter. The triple-slash /// attaches documentation to the item immediately below it. The double-exclamation //! attaches documentation to the module or crate above it.
//! This comment documents the entire crate.
//! It appears at the top of the generated index page.
/// This function adds two integers.
///
/// # Examples
///
/// ```
/// let result = add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
The generated site includes a sidebar with the crate hierarchy, a search bar that indexes every public item, and links between related types. The search is surprisingly powerful. It handles fuzzy matching and finds items by alias.
Convention aside: The community considers cargo doc --open a muscle-memory command. Run it frequently. Don't wait until the crate is "done." Docs drift. Code changes. Running the doc build early catches mismatches between your comments and your API.
The documentation build workflow
The standard workflow is simple. You write comments. You run the command. You review the output.
# Generate docs and open the browser automatically
cargo doc --open
# Generate docs without rebuilding dependencies
cargo doc --no-deps
# Generate docs for a specific feature set
cargo doc --features "serde,async"
The output lands in target/doc/index.html. This directory is not version-controlled. It can grow large, especially with dependencies. Add target/doc to your .gitignore. If you need to publish the docs, use a deployment step that copies the folder to a web server or GitHub Pages.
When you run cargo doc without flags, it documents your crate and all its dependencies. This is useful for exploring the standard library or third-party crates you use. It's also slow. The compiler has to parse and generate HTML for every dependency. Use --no-deps to skip that work. It focuses the output on your code and finishes much faster.
Realistic library documentation
A production library needs more than basic comments. It needs structure, enforcement, and feature awareness.
// lib.rs
// Enforce documentation on all public items.
// This turns missing docs into a compilation error.
#![deny(missing_docs)]
// Hide this module from the public API docs.
// It's internal utility code.
#[doc(hidden)]
pub mod internal_helpers {
pub fn leak_memory() {
// Implementation details
}
}
/// Configuration for the parser.
///
/// This struct is only available when the `serde` feature is enabled.
///
/// # Examples
///
/// ```
/// use my_crate::Config;
///
/// let config = Config::default();
/// assert!(config.is_valid());
/// ```
#[cfg(feature = "serde")]
pub struct Config {
/// The maximum depth for recursion.
pub max_depth: usize,
}
impl Config {
/// Creates a new default configuration.
pub fn new() -> Self {
Self { max_depth: 10 }
}
}
The #![deny(missing_docs)] attribute is a game-changer for library authors. It forces you to document every public item. If you add a new public function and forget the comment, the compiler rejects the build. You cannot ship undocumented APIs. This keeps the public surface clean and helpful.
The #[doc(hidden)] attribute lets you keep items public in the code (so other modules in the crate can use them) while hiding them from the generated HTML. This is useful for internal helpers that shouldn't clutter the user-facing docs.
The #[cfg(feature = "serde")] attribute gates the struct behind a feature flag. If you run cargo doc without enabling the serde feature, the Config struct vanishes from the documentation. Users who enable the feature will see it. Users who don't won't. This is correct behavior, but it causes confusion if you forget to pass the features flag when reviewing your own docs.
Pitfalls and compiler errors
Documentation builds fail for specific reasons. Understanding these failures saves time.
Doctest failures
rustdoc runs every code block in your comments as a test. If the code doesn't compile or the assertions fail, the doc build fails. This is a feature, not a bug. It ensures your examples are always correct.
If you write a broken example, the compiler emits an error like error[E0425]: cannot find value 'x' in this scope inside the doctest output. The error points to the line in the comment. Fix the code in the comment. The docs are part of the contract. If the example is wrong, the contract is broken.
Convention aside: Use # to hide lines in doctests. This lets you include setup code without cluttering the displayed example.
/// Creates a new instance.
///
/// ```
/// # use my_crate::Widget;
/// let w = Widget::new();
/// assert!(w.is_active());
/// ```
pub fn new() -> Widget {
Widget { active: true }
}
The # prefix hides the use statement in the rendered HTML but includes it in the test execution. This keeps examples clean while remaining valid Rust.
Missing features
If your crate uses feature flags, you must pass them to cargo doc. Otherwise, the generated docs look incomplete. Items behind #[cfg(feature = "...")] are invisible.
Run cargo doc --features "all" or list the specific features you want to document. If you maintain a library with many features, consider adding a dev-dependencies section that enables all features for doc generation.
Private items vanish by default
cargo doc only documents public items. Private functions, structs, and modules are excluded. This is the correct default for public crates. Users shouldn't see implementation details.
If you are building an internal tool or a team wiki, use --document-private-items. This flag includes everything in the output. It's useful for onboarding new developers who need to understand the internal structure.
# Document everything, including private items
cargo doc --document-private-items --open
Don't use this for public crates. It exposes internal APIs that users shouldn't rely on. Stick to the default public-only view.
Slow rebuilds
Running cargo doc rebuilds documentation for all dependencies by default. This can take minutes on large projects. The target/doc directory caches the output, but changes in dependencies trigger rebuilds.
Use --no-deps to skip dependency documentation. This focuses the build on your crate. It's faster and produces a smaller output. Use this for local development. Use the full build for final reviews or CI checks.
Decision matrix
Choose the right flags and attributes for your situation.
Use cargo doc --open when you want to review your documentation locally and launch the browser automatically. Use cargo doc --no-deps when you only care about your crate's docs and want to skip the time-consuming dependency rebuild. Use cargo doc --features "..." when your crate has conditional compilation and you need to see items that are hidden behind feature flags. Use --document-private-items when you are building an internal wiki or onboarding guide and need to expose the full code structure. Use #![deny(missing_docs)] when you are publishing a public library and want to enforce that every public item has documentation. Use #[doc(hidden)] when you need to keep an item public for internal use but want to hide it from the user-facing documentation.
Where to go next
Documentation is only as good as the examples it contains. Learn how to write robust examples that serve as both docs and tests.
- How to Add Examples to Rust Documentation
- How to use proptest for property testing
- How to Use cargo-tarpaulin for Code Coverage
Run cargo doc before you commit. Trust the compiler to catch broken examples. Keep your docs alive.