The anatomy of a rustdoc comment
You're reading the docs for std::fs::File. The page is clean. A bold summary. A clear description. Code examples that copy-paste perfectly. A list of methods. You wrote a function yesterday. You added // This does stuff. You ran cargo doc. The output is a wall of text with no structure. You need to know how to turn comments into that professional API page.
Rust documentation isn't written in separate Markdown files. It lives in the code. The rustdoc tool parses specific comment syntax and generates HTML. The sections you see in the browser map directly to how you write those comments. Get the syntax right, and your docs look like the standard library. Get it wrong, and you get a blob.
Think of rustdoc comments as a structured form. The first line is the headline. The next block is the body. Code blocks are the proof. If you skip the headline, the form looks broken. If you skip the proof, nobody trusts you.
Summary, description, and the blank line rule
Every documented item starts with ///. This is the doc comment marker. It applies to the item immediately below it. The first sentence of the comment becomes the Summary. rustdoc uses the summary for the sidebar, search results, and the bold header on the item's page. The summary should be one sentence. It should describe what the item is or does, not how to use it.
After the summary, add a blank line. The text following the blank line becomes the Description. This is where you explain the details, edge cases, and context.
/// A counter that tracks how many times a button has been clicked.
///
/// This struct stores a `u32` internally and provides methods to increment
/// the count and retrieve the current value. It is useful for UI components
/// that need to display a running total without managing state manually.
///
/// # Examples
///
/// ```
/// use my_crate::Counter;
///
/// let mut counter = Counter::new();
/// counter.increment();
/// assert_eq!(counter.get(), 1);
/// ```
pub struct Counter {
// The internal count. Private to enforce access through methods.
count: u32,
}
impl Counter {
/// Creates a new counter initialized to zero.
pub fn new() -> Self {
Self { count: 0 }
}
/// Increments the counter by one.
pub fn increment(&mut self) {
self.count += 1;
}
/// Returns the current count.
pub fn get(&self) -> u32 {
self.count
}
}
Write the summary first. If you can't summarize the item in one sentence, the API is too complex.
Special sections: Panics, Errors, and Safety
rustdoc recognizes specific headings that trigger special rendering. These sections help callers understand failure modes and safety requirements.
Use # Panics to document when a function panics. This is critical for functions that can panic, as panic behavior is not always obvious from the signature.
Use # Errors to document the error types a function returns. This helps callers know which Result variant to expect.
Use # Safety to document unsafe functions. This section is mandatory for unsafe functions. It must list the invariants the caller must uphold. If you omit # Safety on an unsafe function, rustdoc emits a warning.
/// Returns a raw pointer to the element at the given index.
///
/// # Safety
///
/// The caller must ensure that `index` is within the bounds of the slice.
/// Accessing memory outside the slice leads to undefined behavior.
///
/// # Examples
///
/// ```
/// let data = vec![1, 2, 3];
/// // SAFETY: Index 1 is within bounds.
/// let ptr = unsafe { data.get_unchecked_ptr(1) };
/// assert_eq!(unsafe { *ptr }, 2);
/// ```
pub fn get_unchecked_ptr(&self, index: usize) -> *const i32 {
// SAFETY: Caller guarantees index is in bounds.
// We trust the caller to uphold the contract.
unsafe { self.as_ptr().add(index) }
}
Treat the # Safety comment as a contract. If you can't write the invariants, the function shouldn't be unsafe.
Intra-doc links and hidden lines
Documentation is useless if callers can't navigate to related items. rustdoc supports intra-doc links. These are links to other items in your crate or dependencies.
Use @ to link to an item in the same module. Use [Name] to link to an item by name. @ is shorter and less error-prone. The community convention is to use @ for items in the same crate and [Name] for external items or when @ is ambiguous.
Examples often need setup code that clutters the rendered docs. Use # at the start of a line in an example to hide it. The code still compiles and runs, but it doesn't show up in the browser. This keeps examples focused on the usage.
/// Converts this point to a string representation.
///
/// See [`Display`] for the formatting rules.
///
/// # Examples
///
/// ```
/// # use my_crate::Point;
/// let p = Point::new(1, 2);
/// assert_eq!(p.to_string(), "(1, 2)");
/// ```
pub struct Point {
x: i32,
y: i32,
}
impl Point {
/// Creates a new point.
pub fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
}
impl std::fmt::Display for Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
Hide the boilerplate. Show the usage. Use # to keep examples clean.
Module documentation with //!
File-level comments use //!. These apply to the module itself, not an item below. Use //! at the top of a file to document the module's purpose and structure. This renders as the module's summary page.
//! Utilities for parsing configuration files.
//!
//! This module provides functions to read and validate configuration
//! from TOML and JSON files. It handles common errors like missing
//! keys and type mismatches.
//!
//! # Examples
//!
//! ```
//! # use config_utils::parse_toml;
//! let config = parse_toml("config.toml").unwrap();
//! assert_eq!(config.get("port"), Some(8080));
//! ```
/// Parses a TOML file and returns a configuration map.
pub fn parse_toml(path: &str) -> Result<std::collections::HashMap<String, serde_json::Value>, String> {
// Implementation details...
Ok(std::collections::HashMap::new())
}
Use //! for the module root. It sets the context for everything inside.
Pitfalls and compiler feedback
Documentation isn't just text. It's executable code. rustdoc runs all examples as tests. If an example fails to compile or panics, the build fails. This is a feature. It keeps docs in sync with the code.
Common pitfalls include:
- Forgetting
pub. Private items don't get documented. - Using
//instead of///.rustdocignores//. - Missing trait bounds in examples. If your function requires
T: Clone, the example must use a type that implementsClone. The compiler rejects this withE0277(trait bound not satisfied). - Examples that rely on hidden state. If you hide setup code with
#, make sure the visible code still makes sense. - Summaries that are too long.
rustdoctruncates long summaries in the sidebar.
Run cargo test --doc before you commit. Broken examples are worse than no examples.
Decision matrix
Use /// when documenting a public item like a struct, function, or method. Use //! when documenting the module file itself. Use # at the start of a line in an example when you need to hide code from the rendered docs but keep it for compilation. Use @ to link to another item in the same module. Use # Panics when a function can panic and you want to document the conditions. Use # Safety when documenting an unsafe function or block. Use #[doc(hidden)] when an item is public for technical reasons but not part of the public API. Use #[doc(alias = "name")] when you want the item to appear in search results under an alternative name.