The Rust Engineering Handbook / Chapter 50
Rustdoc, Examples, Discoverability, and Feature Documentation
Build Rust documentation that teaches correct use and makes failure, safety, feature, platform, and compatibility conditions retrievable.
A reviewer opens the page for UploadClient::upload and asks five ordinary questions: Can this future move to a worker thread? What does timeout mean? Which errors may be retried? Can the callback reenter the client? Which feature enables diagnostic context?
The page says only:
/// Uploads a request.
pub async fn upload<P>(
&self,
request: UploadRequest,
progress: P,
) -> Result<UploadReceipt, UploadError>
where
P: FnMut(usize) + Send;
The signature contains useful facts, but it does not answer the review. Search cannot retrieve an unwritten cancellation rule. An example cannot compensate for an absent error taxonomy. A crate README cannot rescue an item page viewed from an IDE. Documentation quality is therefore not the volume of prose attached to public items. It is the speed and confidence with which distinct readers can find the contract they need.
For a production Rust library, rustdoc is part narrative, part reference, part executable test suite, and part search index. Design those roles as an information architecture. The goal is not to repeat types in English; it is to expose behavior that types alone cannot encode and connect each reader to a verified path through the API.
Begin the documentation inventory with reader questions, not public-item count. Assign each question one authoritative location, link every secondary entry point to it, and attach an executable witness where compilation can verify the claim. This question → location → witness chain keeps navigation, prose, and doctests from drifting into separate versions of the contract.
Give three readers three routes
Most library documentation must serve at least three jobs:
- A new user needs start here: what the crate does, its central types, the happy path, and the smallest correct example.
- An operator or integrator needs operate: features, platforms, MSRV, blocking, runtime assumptions, resource limits, and behavioral contracts.
- A debugger or reviewer needs diagnose: errors, panics, safety obligations, compatibility changes, and release notes.
Those routes converge on API items, but they should not force readers to traverse the same page in the same order.

The crate root owns orientation and global constraints. Modules explain bounded concepts. Types explain invariants and lifecycles. Functions and methods explain their specific effects and failure conditions. Examples show composition. Release notes explain change. Search terms and intra-doc links connect those layers.
Duplicating one critical sentence can be useful when readers enter at different levels. Duplicating whole narratives creates drift. Put the authoritative feature table at the crate root and link to it from gated items. Put cancellation behavior on the operation that can be cancelled, then summarize the library-wide convention at the crate root.
The crate narrative establishes a reading order
Crate-level //! documentation should answer, in order appropriate to the crate:
- What problem does this crate own, and what does it deliberately not own?
- Which types form the normal path?
- What is the smallest correct end-to-end use?
- Which global contracts surprise experienced users: runtime, blocking, cancellation, safety, platform, MSRV, features?
- Where should readers go for advanced configuration, migration, and diagnostics?
The public-operation-contracts-lab fixture uses its crate root to state the operation boundary, cancellation point, feature flag, platform policy, MSRV, and a runnable upload example. That is appropriate for a focused teaching crate. A larger library should introduce modules and task-oriented guides rather than turn lib.rs into an unstructured manual.
Lead with user vocabulary. A crate named for transport internals may still be searched for “upload timeout,” “idempotency,” or “resume.” Explain domain concepts before listing modules. Rustdoc already renders an item inventory; prose should provide the model the inventory lacks.
Crate docs also need exclusions. If the library does not provide an executor, persistent retry queue, or cross-process idempotency store, say so. Clear non-goals prevent an example from becoming an accidental promise.
Make the first line earn its search position
Rustdoc reuses the text before the first blank line as an item summary in module listings and search-oriented views. Keep it to one direct sentence that distinguishes the item:
/// Uploads owned bytes and returns durable commit evidence.
“Uploads a request” merely paraphrases the name and parameter. The stronger summary tells the reader about ownership and the meaning of success. It does not restate Result<UploadReceipt, UploadError>; it interprets the receipt.
After the summary, document semantics in the order a caller makes decisions: effect, ownership, execution behavior, completion evidence, interruption, failures, and examples. Not every item needs every heading. A pure accessor may need only one sentence. An unsafe function, fallible parser, or effectful async operation needs more.
Use the conventional headings precisely:
# Errorsexplains every caller-relevant error category and recovery decision.# Panicsnames reachable panic conditions, including caller callbacks.# Safetyon anunsafeitem lists obligations whose violation can cause undefined behavior.
Do not add # Safety to safe functions as a vague reassurance. Safe code may have security, cancellation, data-loss, or operational hazards, but those belong under accurately named sections. Conversely, every unsafe fn needs a complete safety contract; examples do not replace it.
Before and after: turn a signature into a contract page
The original upload page fails because it omits decisions. A useful page could begin:
/// Uploads owned bytes and returns durable commit evidence.
///
/// The returned future performs no blocking I/O and is `Send` when `progress`
/// is `Send`. The callback runs synchronously without an internal lock held,
/// may run more than once, and must not reenter this client.
///
/// Dropping before the documented commit point discards local staging. A
/// timeout is not proof that a remote commit did not occur; query by the same
/// idempotency key when the outcome is unknown.
///
/// # Errors
///
/// Returns [`UploadError`]. Retry only when [`UploadError::retryable`] is true,
/// preserving the original [`IdempotencyKey`].
///
/// # Panics
///
/// A panic from `progress` propagates to the polling task.
This is not longer for its own sake. Each sentence answers a branch in real caller code. The intra-doc links make categories and methods navigable and let rustdoc validate their names. The page should then include one example that demonstrates key construction, owned input, progress, awaited completion, and receipt use.
The before page might still compile. Compilation is only one dimension of documentation correctness. The after page adds observable semantics, recovery policy, and navigable relationships.
Examples are small programs with declared truth
A documentation example has three audiences: the reader, rustdoc, and the maintainer changing the API next year. Optimize the visible portion for the reader while keeping the compiled program complete.
A good example:
- begins from imports a user can copy;
- uses domain-shaped values rather than
fooandbar; - shows error propagation rather than unexplained
unwrapcalls; - asserts or uses the important result;
- avoids network, clock, credentials, and global-state dependencies unless those are the teaching subject;
- declares feature, platform, edition, or runtime requirements that affect compilation;
- remains narrow enough that a failure identifies a broken promise.
Examples and doctests overlap but are not identical. A crate’s examples/ programs can show multi-module setup, CLI behavior, tracing, configuration files, or a real executor. They compile as ordinary targets and can be run in integration environments. Doctests sit next to the contract, are excellent for common expressions and rejected uses, and are exercised by cargo test --doc. Use both when the learning job requires both scales.
Do not label an ignored block as verified. ignore prevents rustdoc from compiling it. Prefer a real Rust block, no_run when execution requires unavailable external effects, compile_fail for an intentional rejection, or text for output and pseudocode. Every escape hatch should have a reason.
Hidden lines should remove noise, not causality
Rustdoc compiles lines prefixed with # while hiding them from rendered output. The fixture’s crate example hides a tiny polling helper so the visible example can focus on the upload contract without selecting an async runtime.
That technique is appropriate for imports, error-returning main, deterministic fixture setup, and assertions whose mechanics distract from the point. It becomes misleading when hidden lines create the resource, enable the feature, install the runtime, grant permission, or satisfy the safety precondition that makes the visible code work.
Apply a simple test: if revealing the hidden line would change a reader’s decision about whether or how to use the API, keep it visible or explain it immediately. Hidden setup must not manufacture a false one-line experience.
The hidden polling helper is also scoped as fixture machinery, not production executor advice. The crate narrative says so. A production example would normally show the supported runtime or remain runtime-neutral inside an async fn owned by the application.
Use compile_fail for important rejected designs
Rust documentation should teach what the API prevents when the rejection explains a contract. The fixture includes a compile_fail example whose progress callback captures Rc. Because the API promises a movable future and requires P: Send, the call is rejected.
That doctest guards a useful boundary. If later refactoring removes the Send bound, the example unexpectedly compiles and the doctest fails. It does not assert an exact diagnostic, which would be more brittle than the semantic rejection.
Use compile_fail selectively. Rust can evolve so previously rejected code compiles, and one block may fail for the wrong reason. Keep the program minimal, name the intended reason in prose, pin an edition where necessary, and periodically inspect the actual diagnostic. For complex UI guarantees, a dedicated compile-test harness with expected errors may be clearer than a large doctest.
Rejected examples are especially valuable for unsafe preconditions, ownership transfer, non-Send state, feature-gated items, sealed extension points, and typestate sequencing. They should not become a gallery of beginner syntax errors.
Links turn prose into a checked graph
Intra-doc links such as [`UploadError::retryable`] and [`IdempotencyKey`] keep readers inside the local API graph and can be checked by rustdoc. Prefer them to manually constructed relative HTML URLs. Use explicit paths when names are ambiguous, and read documentation from the re-exported public location because link scope and visible organization may differ from private module layout.
Enable rustdoc lints in CI, especially broken intra-doc links. Treat warnings as defects for public docs. A refactor that leaves prose pointing at an old method is an API usability regression even when library code compiles.
Links should answer a next question. Linking every type occurrence creates noise. Link the first important occurrence, recovery operations, counterpart types, and prerequisites. At the crate root, link the normal journey. On an error type, link the operations that return it and the stable classification method.
External links need durable targets and a maintenance policy. Prefer official Rust documentation for language and tool behavior. For your own contract, prefer versioned local documentation so readers do not need a network to understand a safety requirement.
Feature documentation is part of the API surface
Cargo features change what code exists and what dependencies or capabilities a build receives. Document them at the crate root with a compact table:
| Feature | Default | Enables | Contract and cost |
|---|---|---|---|
diagnostic-context |
no | diagnostic label accessor | adds metadata only; does not change success or retry classification |
For each feature state:
- whether it is enabled by default;
- which public items or behavior it adds;
- optional dependencies it activates;
- runtime, binary-size, compile-time, security, licensing, or platform cost;
- whether it is additive and safe to combine;
- the exact Cargo spelling and a minimal usage example.
Cargo features unify across dependency graph uses, so design and document them as additive capabilities. Mutually exclusive modes are difficult for downstream graphs to coordinate. If incompatibility is unavoidable, reject the combination clearly during compilation and document the supported matrix.
Do not expose an optional dependency’s implicit feature name accidentally when dep:name can keep it private. Feature names become compatibility surface: removing one or moving existing public code behind one can break downstream builds. Release notes must call out feature changes even when function signatures are unchanged.
Conditional items also create documentation-build choices. State which feature set generated hosted docs. Test no-default, default, all-feature, and meaningful combinations rather than assuming all-features represents every user. Experimental rustdoc annotations must be labeled as such; ordinary crate narrative and item prose remain the stable discovery mechanism.
A documentation configuration matrix should accompany the compile matrix:
| Build | Question answered |
|---|---|
| no default features | Can the minimum surface compile, link, and explain how to opt in? |
| default features | Does the experience shown to most users match the crate narrative? |
| every documented additive feature | Does each item appear with the promised contract and links? |
| all features | Do combinations compile and render without duplicate or contradictory pages? |
| hosted-doc feature set | Can readers see its exact deviation from local defaults? |
Run doctests in the configurations that expose them. A feature-gated example tested only under all-features may accidentally depend on an unrelated feature through unification. Conversely, default docs can contain broken links to items that only exist under a hosted configuration. Documentation is a build artifact with inputs; record those inputs as deliberately as compiler flags.
Platform support and MSRV need testable language
“Cross-platform” is not a support policy. Name target families or tiers, required OS facilities, known exclusions, and whether claims mean “compiles,” “tests pass,” or “operated in production.” A crate with no target-specific source may still depend on filesystem semantics, clocks, threading, atomics, TLS roots, or external tools.
Likewise, an MSRV statement must connect to package.rust-version, CI, dependency resolution, and feature combinations. The fixture declares Rust 1.85 and verifies all targets and tests with the installed 1.85 toolchain. That evidence applies to this dependency-free crate and current feature matrix. A library with dependencies must also keep its lock/resolution policy from silently selecting versions above the declared compiler.
Put global platform and MSRV statements at the crate root and package metadata. Add item-level notes when a method has narrower availability. Avoid presenting an unverified target triple list as support. Tell readers what is continuously tested, what is best-effort, and how breaking support changes will be announced.
Examples should reflect the matrix. A Unix-only example needs a platform label or cfg; a feature-gated import must name the feature. Otherwise documentation teaches a configuration that the default reader cannot compile.
Search terms should reflect reader vocabulary
Rustdoc search knows item names, but users search concepts: “deduplicate,” “retry,” “request token,” “deadline,” or a term from another ecosystem. Use those terms naturally in summaries and crate narrative. When a durable alternate name maps cleanly to an item, #[doc(alias = "...")] can improve rustdoc search without polluting the API name.
Aliases are indexing metadata, not a keyword dump. Add them for established synonyms, former public names during migration, protocol terminology, and common spellings. Test representative searches in generated docs. The right metric is whether a reader lands on the authoritative item, not how many aliases an item owns.
Discoverability also depends on re-exports. If common users must navigate private architecture to find UploadRequest, the public module tree is wrong. Re-export primary types from a coherent facade, document them in that public context, and keep implementation-only items out of the visible inventory with privacy first. Use #[doc(hidden)] sparingly for technically public implementation machinery; hiding a usable contract is not documentation design.
Release notes connect versions to changed decisions
API reference describes the current state. Release notes explain movement between states. Record changes that alter caller decisions:
- new or removed feature flags and default-feature changes;
- MSRV or platform support changes;
- error classification and retry policy changes;
- cancellation, blocking, callback, or runtime behavior changes;
- deprecations and replacement paths;
- safety-contract corrections;
- performance changes that invalidate published capacity guidance.
Do not copy the commit log. Organize notes around impact and migration. Link changed items, state whether behavior is compatible, and provide a minimal before/after use when action is required. A documentation-only clarification can be important enough for release notes if prior ambiguity risked duplicate writes or unsafe code.
Keep documentation versioned with code. A reader on an older release needs that release’s feature and safety contract, not the latest website’s. When hosted docs select a non-default feature set, make that visible so item availability is not misread.
Exercise: make every failure and safety condition retrievable
Document a public async upload API, then give the result to a reviewer who has only generated rustdoc and five minutes. The reviewer must identify:
- All stable error categories and the action for each.
- Every documented panic source.
- Whether polling blocks, which runtime is required, and whether the future is
Send. - Cancellation points, commit evidence, timeout meaning, and idempotency rules.
- Callback frequency, concurrency, thread, reentrancy, and panic behavior.
- Every unsafe item and its complete
# Safetyobligations—or proof that none exist. - Default and optional features, combination rules, cost, platforms, and MSRV.
- The release note that would announce a change to any of those decisions.
Deliver a crate narrative, feature/support table, before/after item page, runnable happy-path doctest, one meaningful compile_fail example, one larger examples/ program or a written reason it adds no teaching value, and a search-term map. Run doctests under the declared feature matrix and MSRV. Break one link, change one method name, and remove one bound to confirm the checks fail for the intended reasons.
The exercise fails if the reviewer must read source, infer behavior from a runtime, search an issue tracker, or interpret Display text as a machine contract. It also fails if an example compiles only because a contract-relevant hidden line performs unexplained setup.
Documentation review questions
- Does the crate root orient a new user and declare global operational constraints?
- Does each one-line summary interpret the item rather than restate its signature?
- Can callers find
# Errors,# Panics, and every applicable# Safetycontract at the decision point? - Do examples compile under their stated edition, features, platform, and MSRV?
- Are hidden lines incidental setup rather than concealed causality?
- Do
compile_failblocks reject the intended semantic misuse? - Are intra-doc links checked and public re-exports documented in their visible context?
- Does the feature table describe defaults, costs, combinations, and compatibility?
- Are platform support and MSRV claims tied to an explicit test matrix?
- Do search terms and aliases match established reader vocabulary?
- Do release notes explain behavioral and migration impact rather than list commits?
Design documentation as a verified interface
Good rustdoc compresses the distance between a question and a trustworthy answer. The crate narrative teaches the system, item pages expose local contracts, examples demonstrate composition, doctests guard representative use, rejected examples preserve boundaries, links form a checked graph, feature and platform notes define configurations, and release notes preserve time.
Documentation cannot repair an incoherent API. It can, however, reveal incoherence early: if error behavior, cancellation, ownership, or feature interaction cannot be explained precisely, the design probably lacks a stable contract. Chapters 45–49 supplied those contracts. With them visible and executable, the next unit can conduct an integrated API review and make an evidence-based release decision rather than grading signatures in isolation.
Sources and verification notes
- The rustdoc book: how to write documentation, documentation tests, intra-doc links, rustdoc lints, and advanced search aliases.
- Cargo Book: features, unification, compatibility, and feature discovery.
- Rust API Guidelines: documentation guidance. These are project guidance, not language guarantees.
- Executable source:
examples/rust-engineering-handbook/part-08/public-operation-contracts-lab/. Its crate narrative, item headings, hidden setup, intra-doc links, feature-gated method, runnable doctest, andcompile_failexample are verified with the recorded toolchains. Hosted rendering, target coverage, and real-runtime examples remain independent editorial concerns.
Continue reading
Full table of contents