How to Use Comments and Documentation Comments in Rust

Use // for private code notes and /// for public API documentation that generates help files.

The black box problem

You inherit a crate. It compiles. The tests pass. You open src/lib.rs and see a function called calculate_thing that takes three integers and returns a boolean. The logic inside is a maze of bitwise operations and a magic number 0x7FFF. You have no idea what it does. You change one bit, and suddenly the integration tests fail with a cryptic panic. The code works, but it is a black box.

Rust gives you tools to turn black boxes into glass boxes. Comments and documentation comments are those tools. They do not just explain code to humans. They shape how the compiler treats your API and how other developers interact with your library. In Rust, documentation is not an afterthought. It is executable. It is tested. It is part of the build.

Comments versus documentation comments

Rust has two main comment styles. Regular comments start with //. The compiler ignores them completely. They are for you, your teammates, and future you. Documentation comments start with ///. The compiler also ignores the text, but rustdoc reads them. rustdoc is the tool that generates the HTML documentation you see on docs.rs.

Treat // as a sticky note on your monitor explaining a tricky calculation. Treat /// as the instruction manual printed inside the product box. The sticky note helps you work. The manual helps everyone else use the product.

There is a third style, //!, for module-level documentation. You place //! at the top of a file to describe the module itself. It works like ///, but it attaches to the parent module instead of the next item.

Minimal example

The syntax is simple. The impact is structural.

// Regular comment: ignored by compiler, visible in source only.
// Explains implementation details that do not belong in the API docs.
fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// Doc comment: processed by `rustdoc` to generate public API docs.
///
/// Includes a runnable example that acts as a test.
///
/// # Examples
///
/// ```
/// // The example code runs in its own context.
/// // `add` is available because the function is in scope.
/// let result = add(2, 2);
/// assert_eq!(result, 4);
/// ```
fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

The // comment disappears during compilation. It leaves no trace in the binary. The /// comment survives the compiler but gets picked up by rustdoc. The code block inside the /// comment is not just text. It is a test.

What happens at build time

When you run cargo build, the compiler strips all comments. They do not affect binary size or performance. When you run cargo doc, rustdoc scans for /// and //! comments. It extracts the text, parses the Markdown, and generates HTML files.

rustdoc also looks for code blocks inside doc comments. If you mark a code block with triple backticks, rustdoc treats it as a test. It compiles the example, runs it, and checks for assertions. If the example fails, your documentation build fails. This keeps examples honest. You cannot lie in your docs without the compiler catching you.

Run cargo test to execute doc tests alongside your unit tests. Doc tests appear in the test output with a path like doc::module::function. They are first-class citizens in your test suite.

Realistic API documentation

Real code needs more than one-line descriptions. You need to document behavior, edge cases, and usage patterns. The community follows a convention for doc headers. Headers like # Examples, # Panics, # Errors, and # Safety are recognized by rustdoc. They get special styling in the generated HTML. Always use these headers when relevant. It makes the docs scannable.

/// A simple counter that tracks how many times an event has occurred.
///
/// # Examples
///
/// ```
/// let mut counter = Counter::new();
/// counter.increment();
/// assert_eq!(counter.value(), 1);
/// ```
pub struct Counter {
    count: u32,
}

impl Counter {
    /// Creates a new counter initialized to zero.
    pub fn new() -> Self {
        Counter { count: 0 }
    }

    /// Increments the counter by one.
    ///
    /// # Panics
    ///
    /// Panics if the counter overflows `u32::MAX`.
    ///
    /// # Examples
    ///
    /// ```
    /// let mut c = Counter::new();
    /// c.increment();
    /// ```
    pub fn increment(&mut self) {
        // Using checked_add to enforce the panic guarantee.
        // This makes the panic behavior explicit and testable.
        self.count = self.count.checked_add(1).expect("Counter overflow");
    }

    /// Returns the current count.
    pub fn value(&self) -> u32 {
        self.count
    }
}

The # Panics header tells users exactly when the function will abort. The # Examples header shows how to use it. The example compiles and runs. If you change the function to return a Result, the example breaks, and cargo test fails. The docs force you to update the usage patterns.

Doc tests as living documentation

Doc tests are unique to Rust. In many languages, documentation drifts from reality. The code changes, the docs stay the same, and users get confused. In Rust, the docs are tested. If the docs are wrong, the build is red.

This changes how you write examples. You write examples that actually compile. You write examples that demonstrate the happy path and the edge cases. You write examples that serve as integration tests for your API.

Convention aside: The first line of a doc comment is the summary. rustdoc extracts this for search results and index pages. Write a clear, concise one-sentence summary. Follow it with a blank line, then the detailed explanation. This structure helps users scan the API quickly.

Markdown, links, and code attributes

Doc comments support full Markdown. Use it. Bold terms, lists, and links make docs readable. Links to other items in the crate resolve automatically. Write [HashMap] and rustdoc creates a link to std::collections::HashMap. This creates a web of documentation that users can navigate.

Code blocks have attributes. Add no_run after the backticks to compile the code without executing it. This is useful for examples that require external setup or would delete files. Add ignore to skip compilation. Use ignore only when the code is pseudocode or relies on missing dependencies. Avoid ignore whenever possible. Ignored examples are not tested. They drift from reality and become lies.

/// Starts the server on the given port.
///
/// # Examples
///
/// ```no_run
/// // This code compiles but does not run.
/// // Running it would bind to a port and block forever.
/// server::start(8080);
/// ```
pub fn start(port: u16) {
    // Implementation omitted.
}

Convention aside: Rc::clone(&data) versus data.clone(). Both compile. The community prefers Rc::clone(&data) in examples because data.clone() looks like a deep clone but is not. Be explicit in your docs. Show the intent.

Module documentation

Use //! for module-level documentation. Place it at the top of the file. It describes the module itself, not the items inside. This is crucial for large crates where users need to understand the structure before diving into functions.

//! This module handles user authentication.
//!
//! It provides functions for login, logout, and token refresh.
//!
//! # Overview
//!
//! The authentication flow uses JWT tokens. Tokens expire after one hour.
//! Use `refresh_token` to get a new token without re-authenticating.

pub fn login(username: &str, password: &str) -> Result<String, AuthError> {
    // Implementation omitted.
    unimplemented!()
}

The //! comment attaches to the module. rustdoc places it at the top of the module page. Users see the overview before they see the functions. This guides them through the API.

Pitfalls and failures

Broken examples are the most common failure mode. If you update the function signature but forget to update the doc example, cargo test will fail. The error looks like a normal test failure, but it points to the doc comment. Doc test failures do not have E-codes. They appear as test output. Fix the example immediately. Broken docs erode trust faster than broken code.

Another pitfall is using /// inside a function body. /// only works on items (functions, structs, modules, fields). Inside a function, use //. If you put /// inside a function, it attaches to the next item or does nothing useful.

fn example() {
    /// This is wrong. It is not on an item.
    // Use // here instead.
}

Convention aside: Inside unsafe functions, you have two documentation duties. Use # Safety in the doc comment to tell the caller what invariants they must uphold. Use // SAFETY: inside the function body to explain why the code satisfies those invariants. The doc comment is the contract. The inline comment is the proof.

When to use which

Use // for implementation details that explain why the code is written a certain way. Use // for temporary notes, TODOs, or hacks that you plan to remove. Use // inside function bodies where /// is not allowed. Use // SAFETY: to justify unsafe blocks with numbered invariants.

Use /// for public API documentation. Use /// when you want to generate HTML docs with cargo doc. Use /// when you want to include runnable examples that serve as tests. Use /// on private items if you want internal documentation for your team.

Use //! for module-level documentation. Use //! at the top of a file to describe the purpose of the entire module. Use //! to provide an overview of the module's structure and design.

Use # Examples headers in doc comments to show usage. Use # Panics headers to document conditions that cause a panic. Use # Safety headers for unsafe functions to list the invariants the caller must uphold. Use no_run attributes for examples that compile but should not execute.

Write docs for the reader, not for the compiler. The compiler does not care, but the next developer will.

Where to go next