Skip to content

Rust Documentation and Doc Tests

Every page on docs.rs and every page of the standard library docs was written by a programmer, inside the source code, using special comments. Rust turns those comments into a website with one command. You can do exactly the same for your own code.

There’s a second reason to care. The code examples in your documentation are real tests. When you run cargo test, Rust compiles and runs them. If you change your function and forget to update the example, the test fails. Your documentation can’t quietly go out of date. That’s rare in programming languages, and it’s one of the nicest things about Rust.

You met normal comments on the Comments page. Doc comments look similar but have three characters:

  • /// documents the item below it: a function, struct, enum, field, or module.
  • //! documents the item it is inside. At the top of src/lib.rs or src/main.rs, it describes the whole crate.
//! A tiny recipe program.
/// Splits `total` grams evenly into `portions`.
fn split_evenly(total: u32, portions: u32) -> u32 {
total / portions
}

The first line of a doc comment should be a short summary sentence. Tools show that line in lists and search results, so make it count.

Doc comments don’t change what your program does. Here is a documented function used from main:

/// Splits `total` grams evenly into `portions`.
///
/// Any grams left over are ignored, so 10 grams split 3 ways gives 3.
///
/// # Panics
///
/// Panics if `portions` is zero.
fn split_evenly(total: u32, portions: u32) -> u32 {
if portions == 0 {
panic!("portions must be greater than zero");
}
total / portions
}
fn main() {
println!("{}", split_evenly(600, 3));
println!("{}", split_evenly(10, 3));
}

Output:

200
3
▶ Try it Yourself

In most editors, hovering over split_evenly in main now shows this text in a pop-up. That alone is worth the effort.

Doc comments are written in Markdown, the same format used on GitHub and in many chat apps. You can use `code`, **bold**, *italics*, lists, links, and headings:

/// Returns the oven temperature for a dish, in **degrees Celsius**.
///
/// Supported dishes:
///
/// - `"bread"`: 220 degrees
/// - `"cake"`: 180 degrees
/// - anything else: 200 degrees
///
/// *Always* check your own oven, because ovens differ.
/// See [Celsius on Wikipedia](https://en.wikipedia.org/wiki/Celsius).
pub fn oven_temperature(dish: &str) -> u32 {
match dish {
"bread" => 220,
"cake" => 180,
_ => 200,
}
}

Leave an empty /// line between paragraphs, just like you’d leave an empty line in a normal Markdown file. Wrap names of functions, types, and values in backticks so they show up as code.

Rust programmers use a few headings again and again, so readers know where to look. You’ve already seen one. These are the common ones:

  • # Examples: how to use the item. Almost every public item should have one.
  • # Panics: when the function can panic, so callers can avoid it.
  • # Errors: when a function returning a Result gives back an Err, and why.
  • # Safety: for an unsafe fn, the rules the caller must follow (see Unsafe Rust).

Here are # Errors and # Safety in a library file, src/lib.rs:

/// Reads an amount like `"250g"` and returns the number of grams.
///
/// # Errors
///
/// Returns an error if the text does not end in `g`,
/// or if the part before it is not a whole number.
pub fn parse_grams(text: &str) -> Result<u32, String> {
let number = text
.strip_suffix('g')
.ok_or(format!("'{text}' should end in g"))?;
number
.parse()
.map_err(|_| format!("'{number}' is not a whole number"))
}
/// Returns the first ingredient without checking that the list is empty.
///
/// # Safety
///
/// The caller must make sure `ingredients` is not empty.
/// Calling this on an empty slice is undefined behavior.
pub unsafe fn first_unchecked(ingredients: &[String]) -> &String {
unsafe { ingredients.get_unchecked(0) }
}

Notice that the headings use one #. Inside a doc comment, # is a Markdown heading, not a Rust attribute.

For the rest of this page we’ll work on a small library. Create it with cargo new --lib kitchen. Then, inside the project, run:

Terminal window
cargo doc --open
Documenting kitchen v0.1.0 (/path/to/kitchen)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.91s
Generated /path/to/kitchen/target/doc/kitchen/index.html

Cargo builds an HTML website in target/doc/ and --open opens it in your browser. It looks exactly like the pages on docs.rs: a sidebar, a search box, and one page per function and type. It also documents every crate you depend on, which is handy when you’re offline.

Only public items (marked pub) show up by default, because the docs are for people who use your library. Add --document-private-items to include everything.

Here is the start of src/lib.rs in the kitchen library. The function has an # Examples section with a code block inside the doc comment:

//! Small helpers for the kitchen.
//!
//! `kitchen` converts oven temperatures, splits food into portions,
//! and reads amounts like `"250g"`.
/// Converts a temperature from Celsius to Fahrenheit.
///
/// # Examples
///
/// ```
/// let f = kitchen::celsius_to_fahrenheit(180.0);
/// assert_eq!(f, 356.0);
/// ```
pub fn celsius_to_fahrenheit(celsius: f64) -> f64 {
celsius * 9.0 / 5.0 + 32.0
}

Now run cargo test:

Doc-tests kitchen
running 1 test
test src/lib.rs - celsius_to_fahrenheit (line 10) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Cargo found the code block at line 10, turned it into a tiny program, compiled it, and ran it. The assert_eq! checked the answer. A few things to notice:

  • The example calls kitchen::celsius_to_fahrenheit, with the crate name. A doc test is compiled as if it were someone else’s program using your library, so it only sees pub items, and you need the full path or a use kitchen::...; line.
  • You don’t write fn main. Rustdoc wraps your example in one for you.
  • Use assert! and assert_eq! (from the Testing page) to show and check the result at the same time.

What if the example is wrong? Change 356.0 to 350.0 and run the tests again:

running 1 test
test src/lib.rs - celsius_to_fahrenheit (line 10) ... FAILED
failures:
---- src/lib.rs - celsius_to_fahrenheit (line 10) stdout ----
Test executable failed (exit status: 101).
stderr:
assertion `left == right` failed
left: 356.0
right: 350.0
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

That’s the point. A wrong example is caught right away, instead of confusing a reader months later. To run only the doc tests, use cargo test --doc.

Sometimes an example needs lines that would distract the reader. A line in a doc test that starts with # (hash and space) is compiled but not shown in the generated docs.

The most common use: you want to show the ? operator, but ? only works inside a function that returns a Result. So you hide your own main:

/// Reads an amount like `"250g"` and returns the number of grams.
///
/// # Examples
///
/// ```
/// use kitchen::parse_grams;
///
/// # fn main() -> Result<(), String> {
/// let flour = parse_grams("250g")?;
/// assert_eq!(flour, 250);
///
/// assert!(parse_grams("a lot").is_err());
/// # Ok(())
/// # }
/// ```
pub fn parse_grams(text: &str) -> Result<u32, String> {
let number = text
.strip_suffix('g')
.ok_or(format!("'{text}' should end in g"))?;
number
.parse()
.map_err(|_| format!("'{number}' is not a whole number"))
}

In the browser, the reader only sees the four interesting lines: the use, the ? call, and the two asserts. The test still compiles everything.

Not every example can simply run. You can put a word after the opening backticks to tell rustdoc what to do:

  • ```no_run: compile the example, but don’t run it. Good for code that reads files, opens network connections, or loops forever.
  • ```should_panic: run it and expect a panic. The test fails if it doesn’t panic.
  • ```ignore: don’t compile it at all. Use it for sketches and pseudo-code. Try no_run first, because an ignored example can go out of date without anyone noticing.
  • ```text: not Rust code at all, just text (for example, sample output).
/// Splits `total` grams evenly into `portions`.
///
/// # Examples
///
/// ```
/// assert_eq!(kitchen::split_evenly(600, 3), 200);
/// ```
///
/// Zero portions makes no sense, so this panics:
///
/// ```should_panic
/// kitchen::split_evenly(600, 0);
/// ```
pub fn split_evenly(total: u32, portions: u32) -> u32 {
if portions == 0 {
panic!("portions must be greater than zero");
}
total / portions
}
/// Loads a shopping list from a file, one item per line.
///
/// ```no_run
/// let items = kitchen::load_list("shopping.txt").unwrap();
/// println!("{} items to buy", items.len());
/// ```
pub fn load_list(path: &str) -> std::io::Result<Vec<String>> {
let text = std::fs::read_to_string(path)?;
Ok(text.lines().map(String::from).collect())
}

With all of these in kitchen, plus an ignore sketch in the crate’s //! comment, cargo test reports:

running 7 tests
test src/lib.rs - (line 8) ... ignored
test src/lib.rs - load_list (line 87) - compile ... ok
test src/lib.rs - split_evenly (line 34) ... ok
test src/lib.rs - split_evenly (line 40) - should panic ... ok
test src/lib.rs - parse_grams (line 59) ... ok
test src/lib.rs - celsius_to_fahrenheit (line 16) ... ok
test src/lib.rs - Recipe (line 104) ... ok
test result: ok. 6 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.01s

The no_run test says - compile: it was only built. The order of the lines can change between runs, because tests run in parallel.

Inside a doc comment, write a name in backticks and square brackets, like [`Vec`], and rustdoc turns it into a link to that item’s page. This is called an intra-doc link. It works for your own items and for anything in the standard library:

//! A tiny recipe program.
/// A recipe: a name and a list of ingredients with their weight in grams.
///
/// Create one with [`Recipe::new`] and fill it with [`Recipe::add`].
/// The ingredients are kept in a [`Vec`], in the order you add them.
struct Recipe {
/// The name shown at the top of the recipe.
name: String,
/// Each ingredient and its weight in grams.
ingredients: Vec<(String, u32)>,
}
impl Recipe {
/// Creates an empty recipe with the given name.
fn new(name: &str) -> Recipe {
Recipe { name: name.to_string(), ingredients: Vec::new() }
}
/// Adds an ingredient to the end of the list.
fn add(&mut self, ingredient: &str, grams: u32) {
self.ingredients.push((ingredient.to_string(), grams));
}
/// Adds up the weight of every ingredient.
fn total_grams(&self) -> u32 {
self.ingredients.iter().map(|(_, grams)| grams).sum()
}
}
fn main() {
let mut pancakes = Recipe::new("Pancakes");
pancakes.add("flour", 200);
pancakes.add("milk", 300);
pancakes.add("egg", 50);
println!("{}", pancakes.name);
for (ingredient, grams) in &pancakes.ingredients {
println!("- {ingredient}: {grams} g");
}
println!("Total: {} g", pancakes.total_grams());
}

Output:

Pancakes
- flour: 200 g
- milk: 300 g
- egg: 50 g
Total: 550 g
▶ Try it Yourself

Fields and methods can have their own /// comments too, and each one shows up on the struct’s page. If a link points to something that doesn’t exist, cargo doc warns you. Here Recipe::push was a typo for Recipe::add:

warning: unresolved link to `Recipe::push`
--> src/lib.rs:100:56
|
100 | /// Create one with [`Recipe::new`] and fill it with [`Recipe::push`].
| ^^^^^^^^^^^^ the struct `Recipe` has no field or associated item named `push`
|
= note: `#[warn(rustdoc::broken_intra_doc_links)]` on by default

For a library other people will use, you can ask the compiler to remind you about every public item without docs. Put this at the very top of src/lib.rs:

#![warn(missing_docs)]

Now forgetting a doc comment gives a warning:

warning: missing documentation for a function
--> src/lib.rs:138:1
|
138 | pub fn grams_to_ounces(grams: f64) -> f64 {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: the lint level is defined here
--> src/lib.rs:1:9
|
1 | #![warn(missing_docs)]
| ^^^^^^^^^^^^

The #! (with an exclamation mark) means the attribute applies to the whole crate. It also wants a //! comment for the crate itself. Private items are never reported, so this is not a burden for internal helper functions.

When you publish a crate to crates.io (see Cargo & Crates), docs.rs builds its documentation automatically and hosts it for free, for every version. You don’t have to do anything. That’s why nearly every Rust crate has docs that look the same and are easy to search.

Two more tips:

  • Run rustup doc --std to open the standard library docs offline in your browser.
  • On any docs.rs or std page, click the Source link to read the code behind a function. It’s a great way to learn.

Forgetting the crate name in a doc test. A doc test is outside your crate, so this fails:

error[E0425]: cannot find function `celsius_to_fahrenheit` in this scope
--> src/lib.rs:17:9
|
17 | let f = celsius_to_fahrenheit(180.0);
| ^^^^^^^^^^^^^^^^^^^^^ not found in this scope

The fix is kitchen::celsius_to_fahrenheit(180.0), or a use kitchen::celsius_to_fahrenheit; line at the top of the example (hidden with # if you like).

A doc comment that doesn’t document anything. /// must come right before an item. Inside a function body, use //:

error[E0585]: found a documentation comment that doesn't document anything
--> src/main.rs:3:5
|
3 | /// Split the food into three portions.
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: doc comments must come before what they document, if a comment was intended use `//`

Expecting doc tests in a binary to run. Doc tests run for library crates (src/lib.rs). Examples in the doc comments of src/main.rs are not tested. If your program grows, move the logic into src/lib.rs and keep main.rs small.

Writing docs that repeat the code. “Adds two numbers” above fn add(a, b) doesn’t help. Explain what the reader can’t see: units (grams? seconds?), edge cases, and when it fails.

  • /// documents the next item, and //! documents the crate or module it’s inside. Both use Markdown.
  • Use the standard sections: # Examples, # Panics, # Errors, and # Safety.
  • cargo doc --open builds an HTML site for your crate and its dependencies.
  • Code blocks in doc comments are doc tests: cargo test compiles and runs them. Hide setup lines with # , and use no_run, should_panic, or ignore when needed.
  • [`Name`] creates an intra-doc link, and #![warn(missing_docs)] reminds you about undocumented public items.
  • Crates published to crates.io get their docs hosted on docs.rs automatically.

Quick check

1. Which comment documents the whole crate when placed at the top of src/lib.rs?

2. What does cargo test do with a code block inside a /// comment of a library?

3. In a doc test, what does a line starting with # (hash and space) do?

4. An example opens a network connection. You want it compiled but not run. Which annotation do you use?

Score: 0 / 4