These standards use RFC2119-style keywords to denote requirements, with the understood caveat that interpreting many of these requirements requires greater subjective judgement than would be typical of an IETF standard. I have endeavored to restrict to the use of "MUST" and "MUST NOT" to those requirements that are both fairly objective and unlikely to admit any reasonable excuse for violating them. Nonetheless, this document cannot foresee every possible circumstance, so exceptions to "MUST" requirements may, rarely, be justifiable. However, in any such cases you MUST include a code comment which cites by its [TAG] what rule is being violated and how, and why conforming to it would be impractical.
Violations of "SHOULD" / "SHOULD NOT" requirements do not require such code comments, but similar justification SHOULD be mentioned in PR comments or (if you are an AI model) in the session transcript.
Every item for which rustdoc understands a doc comment MUST include one. This applies to everything: there is no exception for module-private items.
The REQUIRED minimum for everything is a few words explaining what the function does or what the trait/struct/enum/type/field/variant/etc. represents. Exceed the minimum by however much is necessary to make the explanation clear. Modules, traits, and functions are what most often require more than the minimum. Well-designed data structures usually do not.
A crate's top-level module documentation SHOULD be written so as to serve as users' first point of introduction to the crate. Binary crates MUST include at least a paragraph or two explaining what the program does and providing link to more detailed user documentation. Library crates need to be much more thorough, and MUST include at minimum:
- An overview of the crate's purpose and scope.
- A note on the crate's portability, specifiying at least whether the crate:
- Supports
no_stdand works withoutalloc. - Supports
no_stdbut requiresalloc. - Requires
std, but works everywhere that has it, includingwasm32-unknown-unknown(which technically providesstd, but most non-corefunctions just return errors or panic). - Requires
std, but works on (at least) all tier 1 platforms. - …or works only on specific, enumerated platforms.
- Supports
- An overview of how the crate is organized, what types play a central role, and what the crate's main entry points are.
RECOMMENDED for most library crates:
- If applicable: statement and rationale of any particularly opinionated choices about API design.
- Simple usage examples (50 lines maximum), and/or links to the repo's
examples/directory for more detailed ones.
This list is non-exhaustive; crate documentation MAY include any kind of further information useful to include in an introduction.
For small to medium-sized crates, non-top-level module documentation can usually be fairly brief; large crates may need more detailed documentation at the level of every module. Usage examples which only concern the functionality of a single module SHOULD be placed in that module's documentation, not in the crate's top-level documentation. Emulate tokio as an example of a large and complex crate with well-organized module documentation.
A function's documentation plus its type SHOULD be sufficient for a reader to fully understand what the function does (but not necessarily how), without needing to read the function's source code. Depending on the function, such explanation may be very brief, or may need to be very detailed. For example, if you have a method fn inverse(&self) -> Self attached to a struct Matrix, /// Inverts the matrix is plenty of information for the summary line. Nowhere is it necessary to explain that the method creates a new Matrix rather than mutating in place, because that is already implied by the type signature. Nor is it necessary to specify what algorithm is used, since that is an implementation detail, although it may be helpful to state the algorithm's asymptotic complexity. It is necessary for the body of the doc comment to explain what happens if the input is singular.
Documentation of functions which may panic MUST have a # Panics section specifying the conditions under which the panic occurs.
Unsafe functions MUST have a # Safety section specifying what responsibilities the caller must uphold in order to safely call the function.
Functions which return a Result type MAY have an # Errors section detailing the conditions under which an error is returned. The reason that this one is a MAY, rather than MUST, is that the error type's own documentation usually provides sufficient information on its own if you've adhered to [ERROR-TYPES]. The function's documentation SHOULD NOT repeat that information, and provide only information that is specific to the function's behavior. If that information can be provided in a sentence or less, it does need its own heading.
Other conventional documentation headings, such as # Examples and # See Also, are OPTIONAL. Include them if and only if they contribute clarity.
There is almost always more to a trait than just "something which implements this collection of methods": invariants which must be upheld that the method signatures alone do not capture. Trait documentation MUST state these invariants rigorously.
Doc comments on unit tests MUST state affirmatively the assertion that is being tested. Omit "verifies that..." and other such modalities.
Documentation examples MUST compile unless they are specifically intended as negative examples and marked with compile_fail. Use ignore only for code blocks that are not really API documentation at all in the usual sense (e.g. code that is not Rust code, or is code that belongs to some other crate and is included in a compare-and-contrast discussion).
Unless they are marked no_run, documentation examples MUST satisfy the same purity requirements as unit tests (see [TEST-UNIT-PURITY]). However, treat documentation examples as examples first, and tests second. First write the best example you can, and then leave it runnable as a unit test if what you wrote happens to meet the standard required for unit tests. Do not write a worse example for the sake of making it testable.
Comments other than documentation comments SHOULD be infrequent, because most code should be sufficiently self-explanatory that they are not required.
Aside from the justificatory comments discussed under [GENERAL], there is only one case in which non-documentation comments are REQUIRED: every unsafe block MUST be accompanied by a comment tagged with // SAFETY:, stating what invariants the block is relying on for soundness.
Other cases, all of them ideally uncommon, in which comments SHOULD appear, include:
- Comments which explain code that executes an inherently complex and difficult-to-understand algorithm, for which even the clearest implementation cannot stand without exposition. Include citations to academic literature, when applicable.
- Comments which a document a workaround for a bug in another crate. Tag these with
// WORKAROUND:and include a citation to the bug ticket. // TODO:and// FIXME:comments for code that is incomplete or has known issues. Of course, it is better to just address these issues immediately if that is practical.
Algorithmically-oriented code SHOULD, whenever practical, altogether avoid the need for error handling by employing data structures which make erroneous input and states unrepresentable. When importing potentially-invalid input, design a wrapper type which checks the required invariants will not allow itself to be constructed in an invalid state. Give the wrapper type a constructor with a name such as try_new that returns Result<Self, E> for some appropriate error type E, and a convenience method new equivalent to try_new(...).unwrap(). If the constructor takes a single argument, also provide a TryFrom implementation on the type of that argument.
Wrapper types not only make code clearer and safer, but can also make it more efficient. With a wrapper type, the required invariant only needs to be checked once, at construction time, rather than by every function which relies on it.
Error-handling code MUST distinguish between errors that might have originated from outside sources — the user, the filesystem, the network, etc. — from those which could only possibly be the result of bugs internal to the application: whether in the function itself, its dependencies, or its calling code. The first kind of error MUST NOT be handled by panicking; the second kind MUST. In other words, panics are equated with assertion failures, regardless of whether that assertion happens to be spelled assert!, or .expect(...) or .unwrap(), or if (...) { panic!(...) }. It is correct to panic precisely when the detected condition simply should never happen within the codebase, regardless of how much the external environment misbehaves.
It is not necessary or possible to write assertions that catch every possible way in which code could conceivably misbehave. The requirement is rather that, when such assertions exist, the disposition of an assertion failure must be to panic.
"Should never happen" does not quite mean "mathematically impossible". For example, it would be reasonable to assert that a 128-bit loop counter will never overflow, and to panic if it somehow does.
"Internal to the application" means the whole application process, so a library function can treat its caller as "internal" even if the caller is an unknown third-party crate. Such callers' failure to ensure that documented required preconditions are met is fair game for a panic. However, such situations will be rare in practice if you consistently follow the [ERROR-UNREPRESENTABLE] recommendations. It would not violate any "MUST" in these standards for a function which documents that its argument is required to be valid UTF-8, to take that argument as a &[u8] and then panic if it's invalid. However, it would be much better in this case to take a &str, so that the required invariant is already ensured before the function is entered, and any panic that might occur happens earlier on in the caller's code, perhaps while unwrapping a str::from_utf8 result.
Panic messages SHOULD provide a clear indication of what expectation was not satisfied. Unwrapping a Result type that should always be Ok, such as in NonZeroU32::try_from(17).unwrap(), is usually okay, because the TryFromIntError error type will supply the necessary diagnostic information. Use .expect() instead of .unwrap() only if it would provide further information that is useful and not redundant. On the other hand, unwrapping an Option type is usually not okay, because the Option does not provide any diagnostic error type. NonZeroU32::new() returns an Option, not a Result, so you would want to write NonZeroU32::new(17).expect("17 != 0") instead of NonZeroU32::new(17).unwrap().
Error types SHOULD provide as much structured detail as practical. All library code, and some application code, SHOULD define its own error types, preferably using the thiserror crate, for any condition or set of possible conditions that are not already covered exactly by an existing error type. miette::Diagnostic SHOULD be implemented on any error type that would gain something from it, but not when the implementation wouldn't contribute anything and the blanket IntoDiagnostic implementation would produce just as good an error message.
High-level application code which knows that the caller is just going to report the error rather than attempt any fine-grained error recovery that would benefit from structured error types, MAY use miette's anyhow-style macros in lieu of of unwieldy custom error types.
Unit tests MUST be observationally pure. They MUST NOT read or write stdio descriptors, or access the file system, network or loopback interfaces, DBus, or other system resources. They MUST NOT mutate global state, such as by modifying global variables or registering signal handlers. They MUST NOT interact with intra-process resources such as threads or channels unless they created those resources themselves, and they MUST NOT share created resources with other tests.
As a limited exception to this rule, it is acceptable for unit tests to exercise code which triggers global initializers as a possible side effect, provided that those initializers are thread-safe, idempotent, and have no visible effects outside the test process.
Integration tests which run as part of the default cargo test suite MUST be expected to pass in most any test environment, including CI environments where network access is disabled. In contrast to unit tests, they MAY interact with the system in limited and portable ways, such as by spawning subprocesses, creating temporary files, or binding to loopback interfaces. However, they MUST avoid non-portable assumptions about what is available in their runtime environment, such as the presence of a particular system service or library.
Integration tests that do not comply with [TEST-INTEGRATION-PURTY] MAY still be included so long as they do not run by default, and that they provide value by testing behavior that cannot possibly be tested otherwise. However, such tests MUST be configured to run only against dedicated test environments. They MUST NOT access production resources unless that access is read-only, and the resource in question is accessible via the public Internet without authentication of any kind.
Integration tests MUST default to cleaning up all resources they create, such as temporary files. However, since the contents of such files can be useful for debugging, tests SHOULD include an opt-in mechanism for preserving them.
Tests that test features which have been removed from the codebase MUST be removed. Do not retain tests which merely assert that a removed feature is really gone.
Property-based tests which use the proptest crate to assert universally-quantified properties are more useful than tests which just assert a handful of examples of that property, and SHOULD be preferred when possible. Seek opportunities to factor code in ways that make it more amenable to property-based testing.
Large unit test modules which exceed 300 lines of code SHOULD be factored into a separate file rather than being included in the same file as the code they are testing.
Naming things is one of computer science's hard problems. Doing it well is difficult, but substantially contributes to the clarity and quality of a codebase. The best names are concise, evocative, and above all are consistent and reflect a consistent ontology.
Names of impure functions SHOULD begin with a verb. Pure functions SHOULD contain no verb, and be named for what they return; for bool returns this will be an adjective, otherwise a noun or noun phrase. For example, a function which returns a matrix inverse should be named inverse, while one which inverts it in place should be named invert. Sometimes a function is technically pure but it's awkward to think of it that way, in which case it SHOULD be named like an impure function. For example, some cryptosystems are deterministic and some aren't, but either way, names like encrypt, decrypt, sign, verify are more natural than ciphertext, plaintext, signature, etc.
The verb in a function name SHOULD NOT be followed by any noun if the noun is not necessary for disambiguation and can be easily guessed without knowing the function's type signature. For example, sign is better than sign_message, unless the API also deals with signing other kinds of things that are not considered "messages". For a communication API, send and send_to are fine names on their own: it's easy to guess that the first one sends a message to a pre-configured recipient, and the second one takes an argument specifying the recipient.
Names of API functions SHOULD NOT contain adverbs. If you are considering an API with frobnicate_immediately and frobnicate_later functions, then either change these to frobnicate and schedule_frobnicate, or provide a single frobnicate function which takes a when argument.
Verbs SHOULD NOT be conjugated. Hence, schedule_frobnicate, not schedule_frobnicating.
Names of log events emitted via tracing SHOULD essentially be complete sentences, albeit phrased in the usual elliptical register which omits articles, copulas, reflexive subjects, etc. Event names SHOULD NOT contain punctuation unless the punctuation is part of an identifier being mentioned, and SHOULD NOT begin with a capital letter unless the first word is an acronym or proper noun. Words SHOULD be separated with spaces, not underscores or hyphens.
Names of event spans SHOULD typically be shorter than names of events, but still clear and specific, e.g. TCP client handshake. If they contain a verb, the verb SHOULD be conjugated in simple present tense, not as a gerund.
Names of events and event spans MUST be unique per call site throughout the scope of the project workspace. They SHOULD furthermore be semantically unique, not just lexically. You don't need to verify this every time because you should be choosing names that are good enough that their uniqueness is already a safe assumption.
Events and event spans at trace level don't need to follow this naming convention; instead, they SHOULD stick to the default event name that the tracing macros generate from the file and line number of the call site.
Names of keys associated with events SHOULD be styled like the names of function parameters. They SHOULD adhere to a consistent taxonomy that facilitates their use in search queries. The specificity used in naming the key SHOULD be communsurate with the specificity-of-purpose of the code which emits the event. For example, a general-purpose filesystem facility could use a key just named file, but a configuration parser ought to use config_file instead. You SHOULD NOT add qualifiers that are already implied by the overall purpose of the crate. For example, Rust-specific tooling should just use src_file rather than rust_src_file. Key names SHOULD NOT express more than one taxon property. This makes rust_src_file bad for a second reason: if the same hypothetical crate had support for additional languages, it should still just use src_file, and add language=rust as a separate key/value pair.
Most names SHOULD NOT employ abbreviations. If you do use abbreviations, you SHOULD limit them to widely-recognized ones (e.g. "msg" for "message", "config" or "cfg" for "configuration"), and SHOULD use that abbreviation consistently throughout the codebase.
Acronyms are distinct from abbreviations, and SHOULD be used whenever they are standard. In PascalCased identifiers, you SHOULD capitalize only the first letter of the acronym: HttpRequest, not HTTPRequest.
All language identifiers MUST be restricted to printable ASCII. The whole gamut of Unicode MAY be used freely in strings and comments, and doing so is encouraged whenever it is typographically appropriate.
Names of persons SHOULD be rendered faithfully in the person's preferred orthography, including all applicable ligatures and diacritics, if that orthography is based on a Latin script. For non-Latin orthographies, provide both preferred and transliterated forms the first time the name appears in the file, and use the transliterated form thereafter.
APIs SHOULD keep their use of tuple types to a minumum, making exceptions only when it is exceedingly obvious what each component of the tuple represents. Cartesian coordinates are an example of such an exception. Otherwise, instead of using of a tuple, define a struct which gives names to its fields.
Tuples used within the body of a function, without appearing in its signature, are generally fine and are not covered by this rule.
Naming conventions that are widely adhered to throughout the Rust ecosystem SHOULD be followed, even if they conflict with other guidance from this section. For example, it is a widely established convention to use new as the name of a constructor, and to prefix certain sorts of conversion methods with as_, to_, into_, from_, etc. Follow these conventions even where they conflict with [NAMES-FUNCTION].
As an exception to [GENERAL]'s justification requirements, you SHOULD NOT provide justifications for your naming choices unless someone challenges them. Use your best judgement on when to deviate from these guidelines, but do so in silence. Bringing up the topic proactively invites unproductive bikeshedding.
All new projects SHOULD use the tracing crate as their preferred logging framework. Projects SHOULD make use of the tracing-log crate if they have dependencies which emit events through log.
All tracing events MUST be structured, using an event name that is fixed and unique per call site (see [NAMES-EVENT]). Also providing a format string for the human-readable form of the log message is OPTIONAL: usually the name + key/value pairs are easily readable on their own.
Choices of logging levels SHOULD be understood as follows:
error: Something should have happened, but it didn't, because something went wrong in the attempt.warn: Something happened, but may have happened incorrectly.info: Something significant, but normal, happened.debug: Something happened, but probably not of any significance to the user unless the user is trying to debug something.trace: Does not represent that anything has happened; we're reporting on the state of program execution and nothing more.
Anything reported at a level of info or higher SHOULD have some kind of visible consequence from outside the executing process.
Anything reported at a level of debug or higher SHOULD represent some kind of state change, but not necessarily one visible outside the process. For example, writing to a channel or updating a state variable can be reported as a debug event.
debug events SHOULD be added sparingly, only when there is good reason to anticipate that they will be eventually be useful. trace events SHOULD never be added proactively: wait until there is a concrete problem that you are using them to investigate.
Think of trace logging as a more civilized form of printf-debugging. Unlike the cruder form, it is acceptable to commit, but still should not be a permanent feature of the codebase. Once you are confident that a function or module works correctly and have verified this with solid test coverage, you SHOULD remove its trace-level logging. After removing it, consider whether there is some single debug event, meeting the "something happened" threshold, which would have immediately pointed to the problem that you were using the trace events to discover if it had been present. If such a debug event is likely to be useful again in the future, you SHOULD add it.
Functions MUST either dispose locally of abnormal conditions or indicate them through a Result::Err return — never both. An error return means it's up to the caller to decide the disposition of the error. Logging an error is its disposition, or at least a part of it. However, logging at debug or trace is not considered dispositive: functions MAY log at one of these levels and also provide a Result::Err return.
Nothing in the above paragraph should be construed in conflict with [ERROR-VS-PANIC]. Code which needs to panic MUST do so immediately, without logging at the panic site.
Not every Err result is truly an abnormal condition. Crates MUST NOT log errors if the Err is not really abnormal, or if they lack sufficient context to know one way or the other. It would be very bad for a library crate to log every connection failure as an error, if the application using that crate turned out to be a port scanner. The safest policy is for library crates to log only at debug or trace level, and leave info/warn/error to the application.
Log messages and messages to the user interface are separate magisteria. Errors in user input that are handled within the UI and do not result in any action being taken, SHOULD NOT be logged. For example, if a tool can't parse its command line, just let clap report that to the user in its usual way; do not log it.
Messages to the user interface do not need to be structured, and in most cases SHOULD NOT be. If an application is using stderr as part of its user interface, then it is always okay for messages written there to be unstructured, even if the application also has been configured to log to stderr. Users unhappy with the comingling should simply adjust their logging configuration.
Comingling stdout is not okay. If an application can be configured to log to stdout, then it MUST NOT be using stdout for any other purpose.
A client which fails at authenticating to a server will in most circumstances consider that an error. A server failing to authenticate a client is not an error: if a server receives a misauthenticated request and rejects it, then the server is working exactly as designed and nothing has gone wrong. The event is usually not even noteworthy to the server administrator: password-guessers and similar pest traffic on the public Internet are routine background noise. Application context may inform whether it is more appropriate for a server to log authentication failures as info or debug, but error and warn are always inappropriate.
Accepted authentications are the dangerous kind, and (on the server) MUST be logged at least at the same severity as rejected ones. Routine logins SHOULD be info, but consider using warn for administrator accounts that should not be logging in very often.
For high-volume servers where logging can pose a performance concern, the asymptotes of log volume are more interesting than the coefficients. It probably doesn't matter whether a network request or some other IO event produces one log line or five: the interesting threshold is from zero to one. Before adding a log event, think about how it will affect log volume during a DDoS attack. If the event you are recording is something only interesting in aggregate rather than individually significant, consider recording it through the metrics crate rather than through tracing, and then logging only aggregate data at slow, regular intervals. You could also continue to log the individual events at debug level, anticipating that debug logging in production will be turned off, while logging the aggregate data at info.
If crate A's public API uses types that are defined in crate B, then A updating its dependency on B MUST be considered a semver-breaking change to A. Therefore, be deliberate about exposing types defined by third-party crates. If the third-party crates in question are very-widely-used ecosystem crates that aren't likely to make any more semver-breaking changes without extraordinary reason (e.g. tokio, bytes, url), it's fine. For all others, there is some cost to the decision, which you SHOULD mitigate, but cannot eliminate, by re-exporting the whole crate.