Rust best practices
Distilled from five sources, with full per-source digests in
: the Google
Rust Style Guide, the official Rust API Guidelines,
Effective Rust (David Drysdale),
Rust Design Patterns (rust-unofficial), and Microsoft's Pragmatic Rust Guidelines.
Where the sources disagree, this file says which to follow and why.
1. Mindset
- Write Rust-shaped code. Don't port C++, Java, C#, or Python constructs one-to-one.
Model errors with , resources with ownership and , polymorphism with
traits and enums, and nullability with . Inheritance-flavored tricks ( to
a "base" struct, / types, helpers) are smells.
- Make invalid states unrepresentable. Push invariants into types: enums instead of
bool flags and "must be X if Y" comments, newtypes for units and IDs, fallible
constructors that validate once. The compiler then enforces what a comment could only
request.
- Application code is not library code. Library code (anything with external users)
follows the API-design rules below strictly. Application code should favor simplicity
and directness: concrete types, -style errors, return values, no
speculative generality.
- Let the tools do the work. rustfmt owns formatting, clippy owns idiom checks, the
compiler owns memory safety. Spend your attention on design.
2. Before you hand back code
Run these and fix everything they report:
bash
cargo fmt
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo doc --no-deps # libraries: catches broken doc links and missing docs
- Fix a lint rather than silencing it. When a lint is genuinely wrong for one item, use
#[expect(clippy::name, reason = "...")]
on the narrowest item. warns when
the lint stops firing, so stale suppressions get cleaned up. Use only in
generated code or macro output.
- Never put in source. New toolchains add new warnings and will
break the build. Enforce warnings-as-errors in CI instead (
cargo clippy -- -D warnings
or ).
- Configure lints once in (, , or
), not with scattered crate attributes. A good baseline:
,
missing_debug_implementations
, , and where
the project tolerates them clippy::undocumented_unsafe_blocks
and
clippy::allow_attributes_without_reason
.
3. Ownership and borrowing
Prefer unique ownership. Reach for
/
only when shared ownership is part of
the design, and for
/
only when interior mutability is. They are not
failures, but they move checking from compile time to run time, so use them on purpose.
Don't clone to make the borrow checker stop complaining. A clone that exists only to
end a borrow produces a silent copy whose later changes are lost. Try these first, in
roughly this order:
- Extend a temporary with a named , or shorten a borrow with a block.
- Split a long expression into typed intermediates so the failing step is visible.
- Move a field out of with (needs ),
, or instead of cloning it.
- Split a large struct into smaller ones so disjoint fields can be borrowed separately.
The compiler tracks disjoint field borrows inside one function, but not across
method calls.
- Clone small, immutable data deliberately, and say so in a comment if it's not obvious.
- Use indices into an owning , or with back-pointers, as
a last resort. Avoid self-referential structs.
rust
// Move out of a &mut enum without cloning.
fn a_to_b(e: &mut MyEnum) {
if let MyEnum::A { name, x: 0 } = e {
*e = MyEnum::B { name: std::mem::take(name) };
}
}
Accept the most general borrowed type. not
,
not
,
not
,
not
. Deref coercion then lets callers pass
owned values, borrows, or literals. Where you only read a path, string, or byte buffer,
/
/
is fine on parameters (never
on struct fields; store
and
). Library code that only iterates should
take
impl IntoIterator<Item = T>
; application code may simply take
.
Take ownership only when you need it, and then take it by value. If a function stores
or consumes its argument, take
, not
plus an internal
. If it only
reads, take
; if it mutates in place,
. Return multiple values as a tuple or
struct, not through
out-parameters (reusing a caller-owned buffer, as
does, is the exception).
Prefer owned data in structs. A struct holding
forces a lifetime onto every
container. Owning a
or
is usually simpler and rarely the bottleneck.
When a type does carry a lifetime, give it a descriptive name (
,
);
and
are fine on simple functions.
Freeze after setup. Build with
, then rebind
so later
code cannot mutate it by accident.
Keep , , , and out of public signatures. Take
,
, or
and let callers decide how to own things. A service that must be shared
implements a cheap
by wrapping its state in
internally.
4. Types and API design
Naming
| Item | Case | Notes |
|---|
| Types, traits, enum variants | | Acronyms are one word: , |
| Functions, methods, modules, locals | | |
| Constants, statics | | |
| Type parameters | short, from the main bound | , , |
| Lifetimes | short lowercase | descriptive when on a type |
- No prefix on getters: , ,
. Use bare only when there is one obvious thing to get
(). Related: , , for closure
access, and unsafe siblings for checked accessors.
- Conversion prefixes signal cost and ownership: (free, borrow to borrow),
(expensive, borrow to owned or borrow to borrow), (consumes ). Put
in type order: , not .
- Constructors: (primary), / (variants),
(fallible), (conversion; pair important ones with a impl). I/O
resources use domain verbs: , .
- / / return types named / / ;
returns .
- Error types read verb-object-error: , not .
- Avoid weasel suffixes , , , , . Name the thing
it is (, , ). Don't repeat the module name in the
type (, not ). Avoid abbreviations an outsider wouldn't know.
- If you implement , implement .
- Feature names are bare: , , , never or .
Model the domain with types
- Newtypes for semantic distinctions: and
cannot be mixed, where a type alias can. Add when layout matters,
and derive or forward the traits you need ( cuts the boilerplate). A
newtype that encodes an invariant () validates in a fallible constructor
(, , ) and never offers an infallible from the raw type.
- No parameters in public or shared signatures. is unreadable at
the call site and cannot grow a third mode. Use a two-variant enum
(, ) or two methods (,
):
Widget::new(Size::Small, Shape::Round)
, not
. The only exception is a private helper whose call sites sit
a few lines away. When refactoring, an existing parameter is one of the smells to
remove, not a detail to preserve.
- Named fields over tuples, except single-field newtypes and very local code.
- for absence, never sentinels such as or . Data-carrying enums
instead of "field X is only meaningful when Y is set".
- Use the strongest std type: / for paths, for spans,
internally only (keep plain integers at API boundaries).
- Independent flags use the crate; an enum models exactly one choice.
Derive and implement the standard traits eagerly
Downstream crates cannot add std traits to your types (orphan rule), so you must. Derive
whenever every field supports it:
(on every public type; hand-write it to redact
secrets),
(except unique resources and key material),
(only small
bitwise-copyable types, because the compiler copies silently),
/
,
/
(never ship
without
when a total order exists),
(consistent with
),
. Implement
for anything meant to be
read: error types, string-like wrappers. Add
to pure functions and to
types, such as errors and lazy values, whose discarding is a bug.
Implement
/
, never
/
directly (blanket impls give you
those). Prefer
/
over
, which silently allows lossy casts. Implement
rather than an ad-hoc
method,
and
on
collections, and
for both owned and borrowed forms. Implement
only
on smart pointers and owning collections (
to
), never to fake inheritance.
Overload operators only for their mathematical meaning.
Don't put derivable trait bounds on struct definitions:
#[derive(Clone)] struct Good<T>
,
not
struct Bad<T: Clone + Debug>
. Bounds belong on the
blocks that need them.
If you hand-write
, hand-write
,
, and
to agree with it
(compare the same fields, ignore the same fields). A derived
next to a custom
breaks the std contract and misbehaves in
and
/
. Say in the
docs which fields equality ignores.
Constructors, builders, defaults
- Provide even when exists, and make them agree (one delegates to the
other). Derive or implement so callers can write ;
bag-of-options structs with public fields should always have it.
- Up to two optional parameters: inherent constructors (, , ).
Beyond that, or when construction validates or has side effects, add a builder:
returns , setters are named after fields (,
not ) and infallible, and
build() -> Result<Foo, Error>
does all
validation including cross-field checks. Prefer non-consuming setters
() unless must move a value out.
- A constructor with four or more loose arguments should group them into semantic types:
Deposit::new(account, amount)
rather than four / parameters.
Visibility and evolution
-
Private by default. Fields stay private unless the struct is passive C-style data.
Widen to
or
before
. Making an item more public later is
non-breaking; the reverse is breaking, and every
item is a promise.
-
Mark public enums and public-field structs that cross crate boundaries
unless the set is a stable ontology. When matching one, list the
variants you handle and add a wildcard arm.
-
Seal traits you don't want implemented downstream, and say so in the docs:
rust
pub trait TheTrait: private::Sealed { /* ... */ }
mod private {
pub trait Sealed {}
impl Sealed for usize {}
}
-
Hide implementation iterator types behind
(internal APIs) or
a newtype (public APIs you must keep stable).
-
Expose each public item at exactly one path. Don't
a foreign crate's items
unless you are an umbrella crate. Avoid leaking third-party types into your public API;
feature-gate
impls behind a feature named
. If a dependency's type must
appear, re-export it so callers can name the matching version.
-
Mark a function
only if it will stay
. Don't expose
enums
or other internals that lock in failure modes you consider private.
Traits and generics
- Define a trait only for generic programming (, , ) or
as an extension trait (). Don't use traits merely to organize code. Put
essential functionality in inherent methods and have trait impls forward to them, so
callers need no import.
- Prefer, in order: concrete types; generics (, ); ,
ideally wrapped in your own struct (
struct DynStore(Arc<dyn Store>)
). Generics give
static dispatch and inlining; trait objects give heterogeneous collections and smaller
binaries. Don't nest generics on service-level types ( is fine;
is not).
- Decide early whether a trait must be object-safe. Generic methods or outside the
receiver break it; exclude such methods with .
- Keep required trait methods minimal and provide defaults for everything derivable from
them, as does from .
- If a function has a clear receiver, make it a method. A computation with no receiver is
a free function, not . Associated functions are for
construction.
- Closures: use the most general bound that works ( if called once, if it
mutates, else ) and prefer bounds to bare pointers. Closure parameters go
last; don't take more than one.
- Keep parameter order consistent across a crate: call-specific first, ubiquitous
context (loggers, allocators) last.
- Reach for a macro only when functions, generics, and traits cannot do the job. Prefer
to proc macros, make macro input look like the output it generates, and
never let a macro change a type's kind or a function's signature.
Refactoring someone else's code
- Fix every smell the request is about, not just the cheap ones: clones that only
satisfy the borrow checker, / parameters, on fallible
paths, in signatures, getters, parameters, string
concatenation, , error types without , nondeterministic
iteration in output.
- Keep public behavior identical unless changing it is the point: the same operations,
the same success and failure semantics, the same output bytes. Turning a panic into an
is the change callers want. Dropping an error variant, removing a derive, or
changing what a constructor takes is not; if a signature must change, show the before
and after in the change list and say why.
- Preserve quirks you don't understand and flag them, rather than silently "fixing" them.
- When the module has no tests, pin the current behavior with a few before you touch it.
5. Errors and panics
Pick the mechanism by contract:
| Situation | Use |
|---|
| Absence is normal (, ) | |
| The caller may need to know what went wrong | |
| A bug or violated invariant inside your code | / / |
Panics are not exceptions. A panic means "stop now". Never use one to report bad
input, never rely on
for control flow (it does nothing under
and can leave state inconsistent), and never catch a panic and continue
inside a library. Where a panicking convenience is justified, pair it with a fallible API
(
/
,
/
) and document it under
.
Every intentional panic carries a message with the relevant values:
assert!(buf.len() >= HEADER, "buffer too small: {} bytes", buf.len())
,
expect("config is loaded before workers start")
. Prefer
to
. Don't restate the condition as the message.
Pattern-match instead of check-then-unwrap: if let Some(v) = opt { .. }
or
let Some(v) = opt else { return .. };
, never
if opt.is_some() { opt.unwrap() }
.
Lock poisoning is fatal: .lock().expect("lock poisoned")
. Don't recover with
.
Library error types (any
in a public
):
- Implement (which requires + ), be
, and implement for wrapped causes. Never use ,
, or as a library's public error type.
- messages are lowercase, concise, and have no trailing punctuation:
.
- Implement so converts. Use only for foreign errors or
to add context.
- Two sound shapes: an enum with one variant per cause ( generates this
well), or a struct per situation with a private plus and accessor
methods. Either way, don't make one crate-wide enum that lists every failure mode in
the crate, and don't expose details callers cannot act on.
- If a fallible function consumes an argument, return it inside the error so the caller
can retry without cloning (
FromUtf8Error::into_bytes
).
- Never expose in a library's public API.
Application error handling: or
with
.context("opening config")
is
the right tool. Use one app-level error type, not several.
may return
Result<(), anyhow::Error>
.
Destructors never fail, panic, or block. Provide
close(self) -> Result<()>
for
fallible teardown and let
do best-effort cleanup.
Document failure modes: for any
,
for any reachable
panic,
for every
.
6. Everyday idioms
-
Iterators over hand loops when the pipeline is short and reads top-down:
values.iter().filter(..).map(..).collect()
,
,
,
. Collect
fallible steps into
and apply
. Keep an explicit
loop when
the body is large, does several jobs, or needs awkward early returns. Bind a long chain
to a named variable rather than putting it in a
header, and prefer
to
.
-
/ combinators (
,
,
,
,
,
) when they're clearer than
, and
when they're not.
-
/ with inline args (
format!("{name}: {count}")
) for
concatenation and substitution, not chained
or
.
into an
existing buffer in hot paths.
-
Explicit types where inference obscures intent: on
,
,
, and complex iterator results. Prefer
to a turbofish.
-
Drop guards for cleanup that must run on early return or unwind. Bind them to a
name (
), because
drops immediately.
-
Closures that capture mixed ownership: rebind in a block, then
:
rust
let job = {
let cfg = cfg.clone();
move || run(cfg, data)
};
-
Import style: one
per module path (
use foo::bar::{Bar, Baz};
); types and
traits imported and used unqualified (
); free functions qualified by their module
(
,
). Import a trait inside the function that needs it unless the
whole file uses it. No glob imports except
in test modules and
inside a function that matches on it.
-
Magic numbers become named
s with a doc comment saying where the value
comes from and what changing it affects.
-
Logging: structured events through
or
, not
/
,
except for a CLI's own stdout output.
7. Concurrency and async
-
Public types, and the futures they return, should be
(and usually
). Pin
it with a compile-time check so a refactor can't silently regress it:
rust
const _: () = {
const fn assert_send<T: Send>() {}
assert_send::<MyClient>();
};
Holding an
or a
borrow across an
makes a future
.
-
Prefer message passing (channels) to shared state. When sharing is necessary: keep data
with linked invariants under one lock, keep lock scopes to a few lines, never call user
closures while holding a lock, and never return a
from a public method.
-
Write
rather than
, except in traits or when setup must
run outside the async block for a hot path.
-
Long CPU-bound work inside async must yield (
tokio::task::yield_now().await
every
tens of microseconds) or move to
.
-
Library code doesn't spawn raw threads or assume a runtime; take an executor, a spawn
function, or
/
from the caller.
8. Unsafe and FFI
- Write only for one of three reasons: a novel abstraction safe code can't
express, a benchmarked performance need, or FFI and platform calls. Reach for std and
well-reviewed crates first; they already contain the audited .
- Every block or impl gets a comment stating why the invariants
hold. Every gets a doc section listing what the caller must
uphold. Enable .
- Keep in the smallest module that can uphold its invariant and wrap it in a
safe API; the module boundary is the soundness boundary. A safe function that can cause
undefined behavior through any sequence of safe calls is unsound, and unsound code is
never acceptable.
- Mark a function only when misuse can cause undefined behavior, not because it
is dangerous ( is a safe function).
- No to cast enums or dodge lifetimes, and no to silence
the compiler.
- Test unsafe code under Miri and with adversarial inputs.
- FFI: on shared structs, sized integer types, / for
strings, allocate and free on the same side, wrap C-owned pointers in an RAII type whose
frees them, check for null and return , and stop panics at the boundary
with . Generate bindings with in a crate and keep
business logic in a safe core crate.
9. Crates, modules, dependencies
- Prefer small, single-purpose crates in a flat workspace; never nest a
crate inside another crate's . Split when a crate grows a second purpose or a
subdirectory. Share versions and lints through and
.
- Modules: name a leaf module after its primary item ( lives in ).
Non-leaf modules contain only declarations and re-exports. Put the essential items
at the crate root and group the rest by use case (, ), not by kind
(, ). Don't define a prelude.
- Re-export individually (), never with a glob.
- Dependencies: every one costs compile time, binary size, and supply-chain exposure,
including and proc macros. Use ranges (, ), never or a
needless . Commit for binaries. Audit with ,
, , and .
- Features are additive and bare-named: , , , . Never
, , or a feature that removes or changes a public item. Don't
feature-gate struct fields or trait methods. Put mocks and fakes behind .
- Semver: breaking changes include removing or renaming public items, adding a
variant to an enum that isn't , adding a field to a publicly
constructible struct, adding a trait bound, or making a trait non-object-safe.
Deprecate in a minor release before removing in a major one. Use the latest stable
edition for new crates and set an MSRV deliberately.
- Don't run code before (no -style crates). Don't use unstable features
on a stable toolchain.
10. Documentation and comments
- on every public item; at the top of every public module and the crate root.
The first sentence is a single line of about fifteen words, because it becomes the
summary in listings.
- Sections, in this order, as applicable: , , ,
. Examples use with the hidden behind lines, never
, because users copy them verbatim. Describe parameters in prose, not a table.
- Document what the signature can't say: preconditions, side effects, ownership
transfer, complexity, and why. Don't restate the type or the name;
on is noise.
- Link items with . Hide impls users never trigger
(
From<PrivateError> for PublicError
) with .
- comments explain why, not what: , , non-obvious
magic values, workarounds with a link. No design journals, "why we chose X" essays, or
rule-compliance tables in code docs. Document the end state.
11. Tests
- Unit tests in
#[cfg(test)] mod tests { use super::*; .. }
for private internals.
Integration tests in for anything reachable through the public API, and prefer
that location when either would do. Doc examples are tests too.
- Test observable behavior and properties (monotonic, round-trips, idempotent), not
restated constants or mirrored branches.
- Write a failing test before fixing a bug. Cover each feature combination and each
platform-specific path.
- Anything doing I/O, time, randomness, or network must be mockable:
new_mocked() -> (Self, MockCtrl)
behind , not a that performs I/O.
- Fuzz parsers of untrusted input with . Benchmark with or
and . Run Miri over .
- Assertions get context so failures read well:
assert_eq!(got, want, "input {input:?}")
.
12. Performance, in order
Design for usability first and optimize after measuring. When you do:
or
over push loops; reuse caller-owned buffers in hot APIs
(
fn read_into(&mut self, buf: &mut Vec<u8>)
);
,
,
for
long-lived immutable sequences;
on long-lived collections built without a
reservation; a faster hasher (
,
) only for trusted keys; batch work
instead of per-item locking; applications may set a
such as
. Derive
for small plain types and never for large ones.
13. Final checklist
Before returning Rust code, confirm:
- ,
cargo clippy -- -D warnings
, and are clean.
- No outside tests and ; every , , and has a
message with the relevant values.
- Public functions take , , , or , not
or .
- Every public type derives and the applicable common traits; error types
implement
Error + Display + Send + Sync + 'static
.
- No parameters in public signatures; no hand-written without matching
and .
- No whose only job is to satisfy the borrow checker.
- No glob imports, no preludes, no glob re-exports, no leaked third-party types.
- Every block has a comment; every has docs.
- Public items have docs with a one-line summary and / where relevant.
- Application code stayed simple; library code followed the API rules.
References
Read these when you need the reasoning behind a rule or a complete checklist for a task.
Each is a condensed digest of one source.
references/rust-api-guidelines.md
(https://rust-lang.github.io/api-guidelines/):
designing a public API or crate; all 54 checklist items with conventions.
references/effective-rust.md
(https://www.lurklurk.org/effective-rust/): choosing
between alternatives: generics vs , vs panic, iterators vs loops,
borrow-checker tactics, dependencies, CI.
references/rust-design-patterns.md
(https://rust-unofficial.github.io/patterns/):
idioms, patterns, and anti-patterns with code.
references/microsoft-rust-guidelines.md
(https://microsoft.github.io/rust-guidelines/):
error struct design, panic policy, unsafe policy, async, workspace layout, performance.
references/google-rust-style-guide.md
(https://google.github.io/styleguide/rustguide.html):
formatting, imports, module layout, accessor naming, application vs library.