When code isn't enough
You publish a crate. The code works. The tests pass. A developer tries to use it and stares at pub fn process(input: &[u8]) -> Result<Vec<u8>, Error>. They have no idea if input should be JSON, binary, or UTF-8. They don't know if the function allocates, panics on empty slices, or modifies global state. They guess wrong. Their build fails, or worse, their data corrupts at runtime.
Documentation comments bridge the gap between code that functions and code that people can actually use. Rust bakes documentation into the toolchain. You write comments in your source, and rustdoc generates a searchable HTML site. You can also embed runnable examples that double as tests. This integration keeps docs and code in sync. When the code changes, the docs break if you forget to update them.
/// versus //!
Rust provides two syntaxes for documentation comments. The difference is about attachment, not content.
Use /// to attach documentation to the item immediately below it. This works for functions, structs, enums, traits, and modules. The comment becomes part of the item's metadata.
Use //! to attach documentation to the module or crate containing the comment. This is the standard way to document a file. The comment applies to the scope above, not the item below.
//! This crate provides utilities for parsing CSV data.
//!
//! It handles malformed rows gracefully and supports custom delimiters.
/// Represents a single row in a CSV file.
///
/// Each field is stored as a string slice borrowed from the source.
pub struct Row<'a> {
fields: Vec<&'a str>,
}
/// Parses a CSV string into a vector of rows.
///
/// Returns an error if the input contains unescaped quotes.
pub fn parse(input: &str) -> Result<Vec<Row>, ParseError> {
// Implementation details go here.
unimplemented!()
}
The //! comment at the top describes the crate. The /// comments describe the struct and function. If you put /// at the top of a file, it attaches to the first item, which is usually wrong. You end up documenting the first function with the crate overview. Stick to //! for file headers.
The attribute truth
The /// syntax is syntactic sugar. The compiler transforms /// text into #[doc = "text"]. This matters when you need to do something the triple-slash can't express.
You can use #[doc = ...] to inject dynamic strings, though this is rare. More commonly, you see #[doc(alias = "old_name")] to help users find renamed functions. The standard library uses this heavily. If you rename parse_csv to parse, you can add #[doc(alias = "parse_csv")] so that search results still find the function.
Knowing this transformation helps when you encounter #![doc(...)] attributes at the crate level. These control rustdoc behavior, like setting the HTML theme or enabling features for documentation builds. You don't need these often, but they exist because the compiler treats docs as attributes.
Doctests: Documentation that runs
The most powerful feature of Rust documentation is doctests. You can write code examples inside doc comments, and cargo test will compile and run them. This keeps examples alive. If you change the API and forget to update the example, the test suite fails.
/// Calculates the factorial of a number.
///
/// # Examples
///
/// ```
/// use my_crate::factorial;
/// assert_eq!(factorial(5), 120);
/// ```
pub fn factorial(n: u32) -> u32 {
if n == 0 {
return 1;
}
n * factorial(n - 1)
}
The # Examples header is a convention. rustdoc recognizes it and renders the code block as a collapsible section. You can use # Panics, # Errors, and # Safety to structure longer documentation. These headers don't affect compilation, but they make the generated HTML readable.
Doctests run in a separate compilation unit. They simulate a user importing your crate. This isolation is a feature. It catches visibility mistakes. If your doctest passes but a user can't call the function, your API is broken. Doctests force you to think like a user.
Hiding the messy parts
Real-world examples often require setup code that clutters the documentation. You might need to import types, create mock data, or handle Result unwrapping. rustdoc lets you hide lines from the rendered output while still running them.
Prefix a line with # to hide it. The compiler sees the line, but the HTML documentation omits it.
/// Creates a new user with the given name.
///
/// # Examples
///
/// ```
/// # use my_crate::User;
/// # let db = my_crate::Database::mock();
/// let user = User::new("Alice", &db);
/// assert_eq!(user.name(), "Alice");
/// ```
pub struct User {
name: String,
}
impl User {
pub fn new(name: &str, db: &Database) -> Self {
// Store user in database.
Self { name: name.to_string() }
}
pub fn name(&self) -> &str {
&self.name
}
}
The rendered docs show only the User::new call and the assertion. The imports and mock setup are invisible. This keeps examples concise without sacrificing correctness.
You can also control execution with code block attributes. Append no_run to compile the code without executing it. Use this for examples that modify the filesystem or require network access. Append ignore to skip compilation entirely. Use this for illustrative snippets that won't compile due to missing dependencies. Append compile_fail to verify that the code produces a compiler error. This is useful for showing common mistakes.
/// This function panics if the index is out of bounds.
///
/// # Panics
///
/// ```compile_fail
/// let v = vec![1, 2, 3];
/// v.get_unchecked(10); // This would panic at runtime, but we show a compile error example.
/// ```
///
/// Use `get` for safe access instead.
pub fn get_unchecked(&self, index: usize) -> &T {
// Implementation.
unimplemented!()
}
Convention aside: cargo fmt does not format doc comments. You must format the text inside manually. The community standard is to keep doc lines under 80 characters. cargo fmt won't enforce this, so you have to be disciplined. Broken lines in doc comments make the generated HTML look messy.
Pitfalls and compiler errors
Doctests have strict rules. The most common mistake is testing private items. Doctests run as external users. They only see pub items. If you write a doctest for a private function, the test fails with a visibility error.
/// Helper function for internal calculations.
///
/// ```
/// helper(); // Error: cannot find function `helper`
/// ```
fn helper() {}
The compiler rejects this with an error about the function not being found. The fix is to make the function pub or remove the doctest. If the function is pub(crate), the doctest still fails. pub(crate) items are invisible to external crates, and doctests simulate external crates.
Another pitfall is missing imports in doctests. The doctest environment starts empty. You must import everything you use. If you forget an import, the test fails. Use the # hiding syntax to add imports without cluttering the example.
/// Converts a string to uppercase.
///
/// ```
/// # use my_crate::to_upper;
/// assert_eq!(to_upper("hello"), "HELLO");
/// ```
pub fn to_upper(s: &str) -> String {
s.to_uppercase()
}
Without the hidden import, the test fails because to_upper is not in scope. The # line fixes this cleanly.
Module documentation can also trip you up. If you use /// on a module declaration, the docs attach to the module item. If you use //! inside the module file, the docs attach to the module scope. Both work, but //! is preferred for modules because it scopes to the file. If you have a mod.rs file, //! at the top documents the module. If you have a foo.rs file, //! at the top documents the foo module. Consistency matters. Pick one style and stick to it. The Rust standard library uses //! for modules.
Decision matrix
Documentation comments serve different purposes. Choose the right tool for the job.
Use /// when documenting a public item like a function, struct, or trait that users of your crate will interact with.
Use //! when documenting a module or the crate root to provide context, overview, or examples for the entire file.
Use // for internal implementation details, temporary notes, or code that shouldn't appear in the generated API docs.
Use #[doc(hidden)] when an item must be public for technical reasons but shouldn't be part of the stable API surface.
Use no_run in doctests when the code compiles but you don't want to execute it, perhaps because it modifies the filesystem or requires network access.
Use ignore in doctests when the example is illustrative but won't compile, or when the setup is too complex for a snippet.
Use compile_fail in doctests when you want to show an error case and verify that the compiler rejects the code.
Treat documentation as part of the interface. If the docs are wrong, the API is broken. Doctests are the first line of defense against API rot. Run cargo test --doc regularly. If the doctest doesn't compile, the documentation is lying.