These guidelines define IOTA Foundation rules and recommendations for Rust development.
The following rules should always be followed to the best of your ability.
All usages of unsafe, .expect(), panic!(), unreachable!(), or other functions that can explicitly panic under
defined conditions should be justified using a comment, or in some cases in the error message.
Unwrapping should be done using .expect() with an appropriate error message. Messages should follow
these guidelines.
Whenever possible, audited libraries should be used to perform unsafe behaviors, rather than implementing something manually which could be incorrect.
Functions which might panic should document this behavior clearly in its rustdoc comments within a Panics section
(see here for an example).
Use variable names between 1-4 words long that adequately describe the purpose of the variable. Avoid acronyms and abbreviations except when the variable is extremely long.
It is acceptable to use short, non-descriptive variables in some situations. For example:
- Indexes in loops (
i,j,k,idx, etc) - Single-line mapping closures
These rules apply to every comment, and matter especially for comments authored from chat context, which tend to be verbose, restate the visible code, and embed conversational history that becomes stale once the PR is merged.
Function- and item-level doc comments (///, //!, /** */) are written for the caller, not the implementer. They
should answer "what do I need to know to call this correctly?" — nothing more.
Include:
- One short sentence describing what the function or type does, from the caller's point of view.
- Invariants the caller must uphold (preconditions on arguments or state).
- Surprising postconditions, side effects, or ordering guarantees.
# Errors,# Panics,# Safetysections where they apply (see Justify Panics and Document Potential Panics).
Do not include:
- A step-by-step list of what the function does internally — that's the body.
- Rationale for design choices, rejected alternatives, or trade-offs considered.
- References to change history: "added for X flow", "used by Y", "as discussed", PR or issue numbers.
- Restatements of what well-named parameters or return types already convey.
Default to no inline comments inside a function body. A line of code with a good name does not need a comment explaining what it does.
Add an inline comment only when:
- There is a non-obvious why: a workaround for a known bug, a constraint imposed by code elsewhere, or a deliberate departure from the obvious approach.
- An invariant holds at that exact point that a future reader would otherwise have to re-derive.
- The behavior at that line would surprise a careful reader.
Implementation details belong next to the implementation, not in the function header. If a fact only matters at one specific line, comment that line — do not lift the explanation into the docstring.
Never write these, even when asked to "document thoroughly":
- "This function was added to handle the X case discussed with the team."
- "We chose approach A over B because B would have required ..."
- "TODO: revisit per the conversation on YYYY-MM-DD."
- Multi-paragraph rationale blocks attached to a function signature.
- Section headers (
# Implementation,# Notes,# Design) that introduce a content dump rather than a documented contract. - A comment immediately before
let x = foo();that says "Get foo and assign it to x."
If rationale needs to live somewhere, it lives in the PR description, the commit message, or a design doc — never in a doc comment that future readers must scroll past every time they look up the function's contract.
The following doc comment is bad — it records change history, narrates the body, and justifies a design choice:
/// Computes the next epoch's committee.
///
/// This function was added as part of the consensus rework discussed in #1234.
/// It first loads the validator set, then filters out validators that have been
/// slashed, then sorts them by stake, then picks the top N. We chose to sort
/// by stake rather than by VRF output because the team agreed that stake-based
/// selection is more predictable for testing.
pub fn next_committee(epoch: Epoch) -> Committee { ... }The following is good — it states the contract the caller needs and nothing else:
/// Returns the committee that will be active in `epoch`.
///
/// `epoch` must be the current epoch or the immediate next one; older or
/// further-future epochs return `Err(EpochOutOfRange)`.
pub fn next_committee(epoch: Epoch) -> Result<Committee, EpochError> { ... }If the stake-vs-VRF choice matters for a reader of the body, the comment goes inside the function at the sort step — not in the header.
Use crate-level imports. Do not use super to specify relative import paths, as they tend to break more easily during
refactors than crate imports.
NOTE: Relative import paths are allowed in test modules.
Do not use the import-all wildcard (use something::*;). All dependencies should be explicitly imported.
NOTE: The wildcard is allowed for public re-exports.
Error messages should generally be short and to the point. Avoid multiple sentences and periods, instead use commas or semicolons to divide message content. When writing error messages, think about what information would be most helpful in a debugging scenario to quickly understand what the cause of the error was.
Error messages should be lowercase and conform to Common Message Styles.
Errors which are expected to be handled by library users should be defined in such a way that they can be responded to
appropriately, usually by matching on enum variants or an error code. Avoid trait objects and convenience crates such as
anyhow.
Types should be defined as privately as possible, so that updating them does not necessitate breaking changes.
When changes are made to a public API, the changes should be additive if possible.
Public API methods that are due to be deleted should be marked with #[deprecated] with a reason and version, so that
they may be safely deleted after the next breaking version release.
Structs, enums, and variants that are expected to grow (such as error types) should be marked with #[non_exhaustive]
so that additive changes are non-breaking.
Dependencies that are defined in the workspace manifest should follow these rules:
versionset to the most fine-grained version that is non-breaking*default-features = falsefeaturesshould only contain features that are common across all child crates
Dependencies that are defined in crates should follow these rules:
- If using a workspace dependency, set
workspace = true; otherwise setversionto the most fine-grained version that is non-breaking* featurescontains additional features that are needed for the crate
* SemVer specifies that breaking changes may occur on Major releases for stable crates (version >= 1.0.0), and
Minor releases for crates below version 1.0.0.
New dependencies should be well justified.
Unmaintained dependencies should never be added. Replacements should be found for existing unmaintained dependencies if possible.
Never release a version of crates with vulnerabilities reported by auditing. If there is an audit, update immediately when a new version is published.
Critical dependencies should be well vetted. A dependency is critical if replacing it would present a major development effort.
Crates that are commonly used by the community can be considered well-vetted. Lesser-known crates should be thoroughly scrutinized.
Use .unwrap() in tests rather than returning a result, so that the stack trace is printed in the output if the line
fails.
Unit tests should be defined in a tests module as locally as possible to the tested code.
Avoid making non-public APIs public if they will only be used by tests. Instead, either define the tests locally or add
test-specific public APIs that are gated by the #[cfg(test)] attribute.
Test functions should be descriptive and should not be prefixed with test_.
The following conventions are strongly recommended, but may not always apply. Breaking these conventions generally demands an explanation.
In addition to the conventions listed here, follow the Rust API Guidelines.
Generally speaking, mod.rs files should contain little or no code.
NOTE: In situations where a module folder should logically contain functionality, create a file with the same
name as the containing folder and re-export it’s members in mod.rs.
something
├ mod.rs <-- pub use something::*;
⎩ something.rs
Prefer small file sizes with only local struct implementations and strongly applicable code.
When an API is highly reliant on traits to provide common functionality, they should be bundled in a prelude-style public re-export for convenience.
Detailed context should be provided on errors whenever possible.
When using thiserror to define errors, prefer #[source] over #[from] when defining wrapping errors. Errors that
are converted automatically by thiserror contain only the context provided by the wrapped error, which is often from a
3rd party library. This is frequently insufficient to discern the nature of the error when debugging, particularly if a
back trace is not available. Using #[source] forces the call site to manually map error types and allows a opportunity
to add helpful context to errors that may otherwise contain none.
When generics and lifetimes are presented as part of a public API, they should be descriptively named. In particular,
lifetimes should describe what they constrain. Consider using multi-letter names for type parameters (e.g. Doc instead
of just D) as well if it helps clarity and readability, in particular when a type has multiple type parameters.
When exposing a new public API, write accompanying examples and tests that use it.
Documentation about high-level library usage should be located in the top-level lib.rs. This documentation should be
extensive and cover most topics that a user needs to know in order to use the API.
Code examples in rustdoc comments should be used sparingly in code that needs usage examples due to complexity or
non-obvious behaviors.
New features should be strongly justified, and other options should be considered first.