home / skills / mosif16 / codex-skills / rust

Rust skill

/skills/Rust

This skill helps you apply modern Rust best practices for style, error handling, concurrency, testing, and CI across projects.

npx playbooks add skill mosif16/codex-skills --skill rust

Review the files below or copy the command above to add this skill to your agents.

Files (1)
Skill.md
57.3 KB
---
name: "Rust Best Practices Guide"
description: "A comprehensive guide to modern Rust best practices covering style, error handling, performance, concurrency, project organization, dependency management, documentation, testing, security, and CI."
version: "1.0"
dependencies: []
---

# Instructions

## General Coding Conventions and Style

- **Use Standard Naming Conventions:** Follow Rust’s naming idioms for all identifiers. Type names (structs, enums, traits) and enum variants use `UpperCamelCase`. Function, method, module, and variable names use `snake_case`. Constants and statics use `SCREAMING_SNAKE_CASE`. For example, a struct might be named `UserAccount` and a function `process_request`. Avoid acronyms in all-caps in CamelCase (use `Uuid` instead of `UUID`) and prefer full words over abbreviations. If a desired name is a reserved keyword, use a raw identifier (e.g. `r#trait`) or append an underscore rather than misspelling the name.

- **Code Formatting with rustfmt:** Adhere to the official Rust Style Guide (largely embodied by `rustfmt`). Use 4 spaces for indentation and keep line width under 100 chars. Let `rustfmt` automatically format your code to enforce consistent style and avoid bikeshedding in code reviews. In CI, include a formatting check (`cargo fmt -- --check`) to reject misformatted code. This ensures a uniform style across the project, making it easier to read and contributing to community consistency.

- **Idiomatic Expressions:** Embrace Rust idioms for clarity and conciseness. Favor expressions over explicit intermediate variables when possible (e.g. use `let x = if cond { a } else { b };` instead of a mutable `let x;` with an if/else block). Leverage iterators and the ownership system to write clear code rather than low-level loops or manual memory handling when high-level constructs suffice. Write comments in complete sentences and prefer line comments (`//`) for clarity. Use `///` doc comments for public items and include examples and usage guidance in them (see Documentation section).

- **Maintain Module Clarity:** Organize modules hierarchically with clear purposes. Each file in `src/` can define a module; use submodules (with their own files or a `mod.rs`) to logically group code. Avoid using `#[path]` to include files except in unusual cases – instead, follow Cargo’s conventions (e.g. `src/lib.rs` as the crate root, submodules in files named after the module). This makes the project structure predictable. Split large modules into sub-modules to keep files at a manageable size and to separate concerns.

- **Consistent Project Structure:** Use Cargo’s idiomatic layout. A package can contain a library crate (`src/lib.rs`) and/or binary crates (`src/main.rs` for the primary binary, additional binaries in `src/bin/`). Prefer keeping most code in library crates so it can be tested and reused, with the `main.rs` just instantiating and invoking library code. For multi-crate projects, consider a Cargo workspace to share a single `Cargo.lock` and coordinate versions. Each crate should have a clear responsibility. For example, in a web application, you might have separate crates for the core logic, the HTTP server runtime, and utilities.

## Error Handling Best Practices

- **Prefer `Result` for Recoverable Errors:** Rust doesn’t use exceptions; instead, the standard for recoverable errors is the `Result<T, E>` type. Functions that can fail should return a `Result`, giving the caller the choice to handle the error or propagate it. Only use `panic!` for unrecoverable errors or violations of invariants (situations that indicate a bug). For instance, a function reading a file should return `Result<Contents, IOError>` rather than panicking if the file is missing. Use the `?` operator to propagate errors up the call stack conveniently when you want to return the error to the caller.

- **Guidelines on `panic!` vs `Result`:** Call `panic!` only in scenarios where no reasonable recovery exists and continuing execution would be invalid or unsafe. This includes internal logic errors, unreachable code paths, or irrecoverable state corruption. Library code in particular should avoid panicking on expected error conditions; instead, return errors so that the library user can decide how to handle them. In binaries (applications), a panic will crash the program, so reserve it for truly unrecoverable situations. Use `expect`/`unwrap` only in tests, prototypes, or in `main` for quick scripts, and even then prefer providing a helpful message with `expect`.

- **Use Error Types and Context:** Define clear error types for your library using enums (each variant representing a different error case). The `thiserror` crate can derive the `std::error::Error` trait implementation for custom error types easily. For application code (binaries), use a convenient error wrapper like `anyhow` for simplicity. `anyhow::Error` is a dynamic error type that can represent any error and capture backtraces; it’s useful in `main` or higher-level code where fine-grained error handling isn’t needed. For example, a CLI tool’s `main` function can be `fn main() -> Result<(), anyhow::Error>` to use `?` freely with different error types. With `anyhow`, attach context to errors using `.context("high-level description")` or `.with_context(|| ...)` to enrich lower-level errors with more meaning. This helps debugging by clarifying what the program was doing when an error occurred. In libraries, prefer returning structured errors (your own `Error` type or well-known error types) so callers can react accordingly, rather than using `anyhow` which hides error internals.

- **Avoiding Silent Failures:** Do not ignore errors. If a `Result` is unused, Rust will warn. Handle it by propagating (`?`), or explicitly handle both the `Ok` and `Err` cases. If a particular error is truly not actionable, it’s better to at least log it or document why it can be safely ignored. Using methods like `unwrap_or_else` or `if let Err(e) = ...` to log can be useful in those rare cases.

- **Panic Safety and `Drop`:** Be mindful of writing code that panics inside a `Drop` implementation or while holding locks, as this can lead to unintended consequences (like poisoning a mutex or aborting without releasing resources). Prefer not to panic while holding resources; use `Result` to indicate failure in such contexts if possible. Rust’s unwind will run Drop, but if a destructor itself panics, the process will abort.

## Performance Optimization Techniques

- **Zero-Cost Abstractions:** Trust Rust’s zero-cost abstractions – you can write high-level code without a performance penalty compared to low-level code. For example, iterators, closures, and generics are optimized away at compile time to have no overhead beyond hand-written loops. Always prefer clear, idiomatic Rust; the compiler’s optimizations (LLVM) are very powerful. Avoid writing overly complex or micro-optimized code without evidence. High-level iterators often compile to the same machine code as an explicit loop, so use them for clarity and maintainability.

- **Memory Management:** Manage memory by leveraging ownership and borrowing. Whenever possible, allocate data on the stack or in contiguous structures (like `Vec`) rather than using linked structures that fragment heap memory. For most use cases, `Vec` or slices will be more cache-friendly and faster than a `LinkedList`. Avoid unnecessary cloning of data; use references or borrowing (`&T`) to pass data without copying. If you have expensive clone operations, consider using smart pointers like `Rc`/`Arc` (for shared ownership) or `Cow` (copy-on-write) to optimize. However, be careful with `Rc`/`RefCell` – if sharing across threads, use `Arc<Mutex<T>>` since `Rc` is not thread-safe. Use pooling or reuse of allocations if you find allocation overhead in hot paths (e.g., using `Vec::with_capacity` to pre-allocate, or libraries like `bytes` for managing buffers). Rust gives fine control over memory layout; you can use crates like `smallvec` or `arrayvec` to avoid heap allocation in certain cases.

- **Inlining and Compiler Hints:** The compiler will automatically inline many functions at higher optimization levels, especially small ones or generics. Use `#[inline]` or `#[inline(always)]` on functions to suggest inlining in performance-critical spots (e.g. very hot small functions). Inlining can eliminate function call overhead and enable further optimizations across call boundaries. However, measure the impact – inlining can sometimes increase binary size or even hurt performance if it bloats code or prevents other optimizations. Use `#[inline(never)]` on seldom-used heavy functions if the compiler is inlining them unnecessarily. You can also mark cold code paths (e.g. error handling) with `#[cold]` to optimize the hot path better. Always benchmark after tweaking inlining to ensure it helps.

- **Benchmarking:** Use proper benchmarks and profiling to guide optimizations. The Criterion crate is a popular choice for writing robust micro-benchmarks, since the built-in `#[bench]` requires nightly. Profile your code with tools like `perf`, Intel VTune, or Windows’s Performance Analyzer to find true bottlenecks. Also consider using `cargo flamegraph` or similar to visualize hotspots. Optimize the algorithmic complexity first (e.g., use appropriate algorithms and data structures with good complexity), then micro-optimize inner loops if needed. Keep an eye on memory usage and access patterns – memory-bound code may benefit more from cache-friendly structures than from minor tweaks in arithmetic.

- **Zero-Cost FFI and SIMD:** If you need to call external C/C++ code, do so with FFI but ensure you honor Rust’s safety (use `unsafe` and correct `extern` declarations). The overhead of an FFI call is usually minimal (similar to a function call), so it can be zero-cost in abstraction sense, but be cautious about crossing FFI boundary in tight loops. For CPU-intensive tasks, consider using Rust’s stable SIMD support (in `std::arch`) or crates like `packed_simd` for data parallelism, but only after identifying a clear bottleneck that SIMD can address. High-level Rayon (data parallelism) or threads might be more easily applicable for many cases than writing explicit SIMD.

## Concurrency and Async Programming

- **Fearless Concurrency via Ownership:** Rust’s ownership model ensures thread safety by default. Types that are safe to send to other threads implement the `Send` trait, and types safe to share references between threads implement `Sync`. Most primitive types and standard collections are `Send` and `Sync` (as long as their contents are). If you use types like `Rc` or `RefCell` (which are not thread-safe), the compiler will prevent you from sending them to threads. Prefer thread-safe alternatives (`Arc`, `Mutex`, `RwLock`) for shared data. Always design to minimize shared state when possible: favor message passing or channels (e.g. `std::sync::mpsc` or crates like `crossbeam-channel`) to transfer data instead of locking frequently. This reduces contention and possible deadlocks.

- **Threads and Synchronization:** Use high-level concurrency constructs from the standard library for simple multithreading: spawn threads via `std::thread::spawn` for parallel tasks, use `JoinHandle` to join them. Protect shared data with `Mutex<T>` (for mutual exclusion) or `RwLock<T>` (if multiple readers are allowed). Use `Condvar` for more complex waiting conditions. For many cases, message passing is preferable to complex locking — channels provide a safe queue where one thread can send data and another can receive. Keep in mind Rust’s data race safety: once your code compiles using `Send`/`Sync` correctly, you are guaranteed free of data races at runtime, which is a big win. But you should still avoid logic races (race conditions in higher-level logic).

- **Asynchronous Programming (async/await):** For high-concurrency networking or I/O-bound tasks, consider using async/await. Async in Rust is cooperative concurrency: an async runtime like **Tokio** or **async-std** runs many tasks on a few threads by polling futures. **Tokio** is the most widely used async runtime in the ecosystem, offering a rich ecosystem (timers, networking, etc.) and a multi-thread scheduler by default. **async-std** provides a similar API inspired by Node.js and is an alternative for certain use-cases, but Tokio has become the de facto standard for production systems. When writing async code, use `#[tokio::main]` (or equivalent) to start the runtime and `.await` on `async fn` futures. Ensure tasks you spawn are `Send` (Tokio’s multi-threaded scheduler requires spawned futures to be `Send` by default). If you have a !Send future (for example, it uses `Rc` instead of `Arc`), you must run it on a current-thread runtime or use `tokio::task::LocalSet` (Tokio also provides `spawn_local` for such cases). Concurrency in async code comes from having multiple tasks running and yielding control at `.await` points.

- **Async Concurrency Considerations:** Although async tasks run on a thread pool, you must never perform long blocking operations inside an async function, as it will block the entire executor thread. For any blocking I/O or CPU-bound work, offload to a dedicated thread pool (Tokio provides `spawn_blocking`) so you don’t stall other async tasks. Use synchronization primitives designed for async if needed: e.g., `tokio::sync::Mutex` instead of std `Mutex` (to prevent blocking the reactor), or `tokio::sync::mpsc` for async channels. Async code can still have race conditions in logic (like two tasks updating a shared resource if not synchronized), so use tools like `Arc<tokio::sync::Mutex<_>>` or channels to coordinate. Also be mindful of cancellation: an awaited future might be cancelled if the caller drops it. Write cleanup code or use `drop_guard` patterns if necessary to handle half-done operations. Leverage higher-level libraries (like Tokio’s `select!` macro, or `futures` crate utilities) to manage multiple concurrent tasks and timeouts.

- **Send and Sync Marker Traits:** Understand the `Send` and `Sync` traits as they relate to concurrency. `Send` means a value can be transferred safely to another thread, and `Sync` means a reference to a value is safe to share between threads. Rust automatically implements these for types that are composed of `Send`/`Sync` parts. When writing unsafe code or interfacing with FFI, ensure custom types correctly implement (or do not implement) these markers to uphold thread safety. Never implement `Send` or `Sync` for your type unless you are absolutely sure of its thread safety — incorrect manual implementation can break Rust’s guarantees (such manual impls are `unsafe`). Typically, rely on the compiler’s auto-implementation, and use `std::marker::PhantomData` if you need to indicate that your type contains some `T: Send` etc.

## Project Structure and Module Organization

- **Crates and Packages:** A *crate* is a compilation unit in Rust, and a *package* (Cargo package) is a set of one or more crates with a Cargo.toml. Use separate crates to clearly separate concerns or to enable reuse. For example, a project might have a core library crate and binary crates that use that library. Each crate has a *crate root* source file (like `src/lib.rs` or `src/main.rs`). By default, `cargo new` creates a package with a binary crate (`main.rs`). If you also add a `src/lib.rs`, that defines a library crate of the same name. Use this library for most code and keep `main.rs` minimal, which facilitates testing and reuse.

- **Modules and Visibility:** Use modules (`mod`) to organize code within a crate. Modules let you control scope and privacy. Start with a high-level module for each major component of your program (e.g. `network`, `database`, `handlers`, etc., as submodules in your library). Inside each module, group related types and functions. By default, items in a module are private; use the `pub` keyword to expose items at the module boundary as part of your crate’s public API (for libraries) or to other modules (for binaries). Strive for a clean public API in libraries – the Rust API Guidelines encourage keeping implementation details private and exposing a consistent, minimal interface.

- **File Organization:** Follow Cargo conventions to map modules to file system. For example, `mod parser;` in `lib.rs` will load code from `parser.rs` or `parser/mod.rs` in `src`. Organize submodules as either separate files (if the module is small) or subdirectories with a `mod.rs` (or use the newer inline file naming, e.g. `parser/mod.rs` can also be just `parser.rs` plus submodules in `parser/`). Consistency is key; many projects now avoid `mod.rs` files and use the flat file approach (Rust 2018+ allows a module in `foo.rs` with submodules in `foo/bar.rs` without a mod.rs). Choose one style and stick to it. For larger projects, group crates in a Cargo workspace to share common Cargo settings and dependencies. Each crate in a workspace goes in its own subdirectory, and the workspace `Cargo.toml` aggregates them.

- **Module Privacy for Organization:** Use `pub(crate)` and `pub(super)` effectively to manage the visibility of internal helper functions and structs. This way you can test internal implementation details (by declaring tests in the same module or using `#[cfg(test)] mod tests`) while still hiding them from the public API. This leads to a cleaner API surface for libraries and encapsulation of logic.

## Dependency Management and Cargo Features

- **Cargo.toml Best Practices:** Pin your dependencies by version using Cargo’s semantics (Caret requirements by default, e.g. `foo = "1.2.3"` means compatible with `1.x`). Respect Semantic Versioning in dependencies: by default, Cargo will pick the latest semver-compatible version. For binary applications, it’s recommended to check in the `Cargo.lock` to ensure reproducible builds (the lockfile fixes exact versions). For libraries, historically the practice was to ignore `Cargo.lock` so that downstream crates can use the latest compatible versions. Recently, the guidance has softened to “do what is best for your project” – you may commit a lockfile in a library for CI testing consistency, but it will be ignored by cargo when your library is used as a dependency. In either case, make sure to regularly run `cargo update` and test with the latest versions of your deps to catch any breakage early (CI can help automate this). 

- **Use of Features for Optional Dependencies:** Leverage Cargo *feature flags* to make dependencies optional and to control conditional compilation. Mark rarely-used dependencies as `optional = true` in Cargo.toml and group them under named features. For example, if your crate has an optional JSON serialization capability, you might have: `[dependencies] serde = { version = "1.0", optional = true }` and `[features] json = ["serde"]`. This way, users can opt-in by enabling the `"json"` feature, and it keeps your crate lightweight by default. Name features clearly and positively (features should *add* capabilities) – avoid negative naming like "no-std" (instead use a feature named "std" that is on by default and can be turned off). Features are unified across the dependency graph (if any crate enables a feature of your crate, it will be enabled for all users of your crate in that program), so design them to be additive and compatible. Document your features in the README or documentation so users know how to enable optional functionality.

- **Avoid Dependency Bloat:** Each dependency can increase compile times and binary size. Prefer lightweight crates and standard library where possible. Before adding a new dependency, consider if it’s truly needed or if it could be feature-gated. Pay attention to your dependency tree (`cargo tree` and `cargo tree -e features`) to identify large or duplicated dependencies. Use Cargo features to cut down unused parts of dependencies (some popular crates have features to disable default heavy functionality). For example, if using `serde`, you can disable its default features and enable only `serde/derive` if you just need the derive macros. Also watch for build-only dependencies and dev-dependencies; keep dev-dependencies limited to non-production needs (they won’t be included in downstream crates but still affect your compile times).

- **Handling Version Conflicts:** Thanks to Cargo’s version resolution, you can generally rely on semver to keep dependencies working. In cases where you need specific versions (e.g., security fixes or API changes), you can use inequality requirements (`>=`, `=`, etc.) or direct `git` or `path` dependencies for temporary overrides. However, prefer to work with maintainers/upstream to keep using released versions. If your library is used widely, consider having a policy for minimum supported Rust version (MSRV) and note it in your Cargo.toml (with a `rust-version` field) and documentation, so dependency upgrades don’t accidentally require newer compilers without warning.

- **Workspaces for Multi-Crate Projects:** If your project includes multiple interdependent crates (e.g., a library and several binary utilities), use a Cargo workspace. This allows shared `Cargo.lock` and output directory, making builds faster and dependency versions consistent across crates. Put a `Cargo.toml` in the root with `[workspace]` members listed, and remove individual `Cargo.lock` files in members (workspace uses one at the top level). Workspaces also make it easier to run commands across all crates (like `cargo fmt` runs on all members).

## Documentation and Testing Conventions

- **Write Rustdoc Documentation:** Every public item (public structs, enums, functions, modules, etc.) should have a **rustdoc** comment (`///`) explaining its purpose, how to use it, and any important details. At the crate root (`lib.rs` or in `main.rs` for binaries), include a module-level doc comment giving an overview of the crate’s functionality and usage examples (for libraries, this is the first page users see on docs.rs). Aim to include examples in docs for all major functionality. The Rust API Guidelines recommend including examples for virtually every public item if possible. Use doctest code blocks in documentation: any code in triple backticks with `rust` will be tested by `cargo test` to ensure it stays correct. For example:
  ```rust
  /// Calculates the area of a rectangle.
  /// 
  /// # Example
  /// ```
  /// let rect = Rectangle::new(5, 10);
  /// assert_eq!(50, rect.area());
  /// ```

This example will be compiled and run as a test (outputs are not required unless you use ///# lines to hide setup). Ensure your doc examples prefer the ? operator for fallible calls rather than using unwrap() or expect(), so you encourage proper error handling. You can write your examples in a way that they compile as a complete program by using hidden # use ...; fn main() -> Result<(), Box<dyn std::error::Error>> { ... } wrapper, which will not show in docs but allows using ?.
	•	Document Panics, Errors, and Safety: If a public function can panic! (e.g. it has internal unwraps or it will panic on invalid input), document this in a “# Panics” section in the doc comment. Similarly, if your function returns a Result or has error conditions, add an “# Errors” section explaining under what conditions errors are returned. For unsafe functions or trait implementations, include a “# Safety” section that explains the invariants the caller or implementer must uphold. For example, if you have unsafe fn do_io(ptr: *mut u8, len: usize), document that the pointer must be valid for len bytes, etc. This documentation is crucial for users to know how to use your APIs correctly and for future maintainers (including yourself) to remember the reasoning. Clippy has a lint to warn if you have pub unsafe fn without a # Safety section in the docs, which is a good practice to enable.
	•	Testing Strategies: Follow the Rust testing conventions: write unit tests in the same file as the code by adding a #[cfg(test)] mod tests module. Unit tests should focus on small units of functionality (individual functions or types). Use #[test] functions to define tests. Use assertions (assert_eq!, assert!, etc.) to verify behavior. For tests that need to check for errors, you can have tests return Result<()> and use ? for convenience (any test that returns Result which Err will fail the test).
	•	Integration Tests: For larger scope testing, or tests of the public API from an external perspective, use integration tests in the tests/ directory. Create tests/*.rs files; each file is compiled as a separate crate that automatically depends on your library crate. In these, import your crate (e.g. use my_crate::*;) and write tests as if you were an external user. Integration tests ensure that your public API is usable and works end-to-end. They’re especially useful for testing binary applications (e.g. using assert_cmd crate to run your CLI and check output). Running cargo test will compile and run all unit tests, integration tests, and will also run doc-tests extracted from your documentation. Keep integration tests in separate files focusing on different areas (for example, tests/api.rs for testing API endpoints, tests/cli.rs for CLI interface tests, etc.).
	•	Documentation Tests: As noted, doctests are automatically run. Ensure that your example code in docs actually works by running cargo test. Sometimes you may need to skip or ignore certain doc tests (for example, if they are meant to fail or are long-running) by adding /// ```rust,ignore or using no_run (compiles but doesn’t run). Use these sparingly; ideally, doc tests should run if possible, since they double as examples and tests.
	•	Test Organization: Name your test functions clearly to indicate what they cover. You can group related tests using sub-modules or simply by proximity in code. If common setup is needed for multiple tests, consider using the standard library’s support for test fixtures (you can do setup in the test functions themselves or extract a helper function; there’s no built-in JUnit-style setup/teardown, but you can leverage Rust’s scope rules to setup and drop resources).
	•	Continuous Testing: Run your test suite often (e.g. via cargo test or tools like cargo watch to run tests on file changes). Maintain high coverage of critical code. Use code coverage tools (like cargo tarpaulin or cargo LLVM coverage) to identify gaps, though do not chase coverage numbers at the expense of meaningful tests. Focus on edge cases, typical usage, and regression tests for fixed bugs.

Security and Safety Considerations
	•	Avoiding unsafe where Possible: Rust’s safety guarantees are strongest when you stay in safe code. Mark sections of code as unsafe only when absolutely necessary (e.g., interfacing with C, implementing a data structure that the borrow checker can’t understand, or calling a performance-critical low-level instruction). Every unsafe block or function is a promise that you have manually verified the code upholds Rust’s safety rules (no data races, no invalid pointer dereferences, proper alignment, no use-after-free, etc.). When you use unsafe, encapsulate it – hide it behind safe abstractions if you can, so that the unsafety doesn’t leak out to users. For example, if implementing a memory pool, keep all unsafe inside the module and expose safe functions that internally ensure safety invariants.
	•	Follow Unsafe Code Guidelines: If you must write unsafe code, follow the emerging Rust Unsafe Code Guidelines and consult the Rustonomicon (The Dark Arts of Unsafe Rust) for best practices. For example, one guideline is to document all safety requirements of an unsafe function (whoever calls it must uphold those) clearly in comments and docs. Another is to minimize the size of unsafe blocks – perform just the unsafe operations inside, and do as much as possible in safe code. Use tools like Miri (an interpreter that can detect undefined behavior) to test your unsafe code for issues. Run with sanitizer support (e.g., ASan or ThreadSanitizer via Rust flags) when debugging memory issues.
	•	Clippy Lints: Use cargo clippy regularly to catch common mistakes and improve your code’s idiomatic style. Clippy is a collection of lints for code correctness, performance, style, and more. It can warn about things like using unwrap in code (which might be better handled via proper error handling), or using inefficient methods when a better one exists, etc. For instance, Clippy will suggest using iter().any() instead of searching for an element with a manual loop, or it might catch when you clone an Option or Result unnecessarily. Enable at least the clippy::pedantic or clippy::nursery lints if you want more strict checks, though be aware some lints are opinionated. In CI, you can treat Clippy warnings as errors (cargo clippy -- -D warnings) to ensure all suggested improvements are addressed. If certain Clippy lints are not applicable or too noisy for your project, you can explicitly allow them (via attributes or a clippy.toml config). The key is to use Clippy to learn idiomatic patterns and catch potential errors (like forgetting to implement std::fmt::Debug or using == on floating-point numbers directly, etc.).
	•	Limit unsafe and Audit It: If possible, keep a crate-level lint #![forbid(unsafe_code)] for crates that do not need unsafe at all – this ensures you don’t accidentally introduce unsafe. If your project must use unsafe (e.g., in a low-level module), consider isolating it in a small part of the codebase. Have code reviews pay special attention to unsafe blocks. Use external audit tools or have peers review those sections thoroughly. The community’s philosophy is to acknowledge that unsafe is sometimes necessary for high performance or FFI, but it should be handled with extreme care and scrutiny.
	•	Memory Safety and Ownership: Embrace the ownership model to naturally avoid entire classes of bugs (buffer overflows, double frees, etc.). If interfacing with C code or doing manual memory management, always be vigilant to convert raw pointers to safe Rust types as soon as possible. For example, if you get a raw pointer from C, wrap it in a slice::from_raw_parts or similar unsafe block to create a slice, then operate on that safely. Avoid mutable global state; if you need global state, use the lazy_static or once_cell crates to initialize it safely, or better, refactor to pass state through functions. Prefer higher-level concurrency primitives to avoid deadlocks and other thread safety issues; for instance, rather than sprinkling Arc<Mutex<T>> everywhere, consider designing threads or tasks so that each has exclusive ownership of the data it needs as much as possible.
	•	Security Audits and Tools: Use tools like cargo audit to scan your dependencies for known security vulnerabilities. The RustSec advisory database will warn you if any crate version you use has a security flaw. Integrate cargo audit in CI (it’s quick to run) so you get notified when a new vulnerability is disclosed. Keep your dependencies up-to-date, especially for security fixes. For cryptographic or security-sensitive code, prefer to use well-reviewed crates (like ring for crypto, or sodiumoxide, etc.) instead of writing your own unless you have the expertise. If you handle sensitive data, be aware of side-channel considerations (e.g., use constant-time comparisons for secrets) and the fact that standard memory might not be zeroed on drop (use zeroize crate for that).
	•	Lint for Suspicious Constructs: Rust’s compiler and Clippy provide lints that help security: e.g., the compiler will warn on unused must_use results (often errors that were not handled), which can catch logic mistakes. Clippy can warn about using println! in libraries (which might be a debug leftover), or having debug (dbg!) macros, etc. Consider enabling #![deny(warnings)] on release builds to catch any warnings (though for CI it’s often better to use Clippy as above). There’s also cargo-deny which can check for license compliance and other security-related policies in dependencies.

Code Review and CI Practices
	•	Code Reviews Focus: In Rust code reviews, focus on correctness, clarity, and idiomatic usage. Ensure error handling is done for all possible failure points (no .unwrap() or .expect() in library code, and any uses in application code are justified). Check that functions have documentation, especially public ones, and that naming is clear. Verify that unsafe blocks (if any) are sound – reviewers should attempt to reason about each unsafe usage or request an explanation if it’s not obvious. Look out for overly complex code; often there might be a simpler idiomatic approach (Clippy hints can guide here). Ensure tests cover new code, and that all tests pass. If performance is a concern for the changed code, discuss whether benchmarks exist or should be added, or whether the change has been profiled.
	•	Automate Checks in CI: Set up a Continuous Integration pipeline that runs on each pull request and push to main. The CI should at minimum run cargo build (to ensure code compiles on a clean environment for all target features), cargo test (including doc tests and integration tests), cargo fmt -- --check (to enforce formatting), and cargo clippy (to enforce lint cleanliness). For clippy, you might use -D warnings to make any warning fail the CI build. This ensures that all merged code adheres to the standards automatically. Additionally, consider running cargo audit in CI to catch dependency vulnerabilities early. If the project is a library, you can have CI test on multiple Rust versions (especially MSRV if you promise compatibility with older compilers, and the latest stable) and possibly different platforms if relevant (Linux, Windows, Mac, etc., or even WASM targets if you support that).
	•	Continuous Deployment / Integration: If applicable, use CI to run additional quality checks: e.g., fuzz testing using cargo fuzz (to catch crash or panic cases), property-based tests (with proptest crate), or integration tests in a staging environment. These might not run on every PR, but could run nightly or on demand. For libraries, also ensure your examples compile (cargo test --examples) and perhaps that cargo doc succeeds without warnings.
	•	Pull Request Checklist: It’s useful to have a PR template or checklist that reminds contributors of common things: updated documentation for any user-facing change, tests for new features or bug fixes, adherence to style guidelines (though CI will catch formatting/clippy issues). This keeps the review process smooth. Encourage small, focused PRs when possible, which are easier to review thoroughly.
	•	CI Artifacts and Coverage: Optionally, use CI to generate and upload documentation (docs.rs already builds docs for releases of libraries). For applications, CI can build binaries for various targets (using cross-compilation or matrix builds) and even run a security audit or static analysis. Some projects enforce that new code has some minimal test coverage or at least doesn’t reduce coverage – tools like Codecov can be integrated to report on PRs. While code coverage isn’t a perfect metric, it can help identify untested code paths.
	•	Release Practices: When ready to release (for libraries, publishing to crates.io; for binaries, cutting a new version/tag), ensure your CI or release process runs the full test suite and possibly lints in release mode. Consider using cargo test --release for some performance-sensitive tests (though most logic tests run in debug fine). Use CI to publish artifacts or deploy as needed, and automate as much of this as makes sense (for example, use cargo publish in CI with proper credentials for libraries, or build and attach binaries to GitHub Releases for apps).

By following these guidelines – writing clear, idiomatic code, handling errors robustly, optimizing wisely, using Rust’s concurrency safely, structuring projects cleanly, managing dependencies thoughtfully, documenting thoroughly, and leveraging CI – you can create Rust code that is reliable, maintainable, efficient, and a joy to work on. Happy Rusting!

Workflow

Below is a step-by-step workflow to apply these best practices in a Rust project:
	1.	Initial Setup: Start a new project with cargo new. Immediately set up version control (e.g., git) and add a continuous integration configuration (GitHub Actions, GitLab CI, etc.) to run tests, formatting, and Clippy on each push. Enable rustfmt and clippy in your development environment (via rustup component add rustfmt clippy).
	2.	Establish Conventions: Create a rustfmt.toml if needed to tweak formatting (or stick with default). Document naming conventions and module layout in a CONTRIBUTING.md or internal wiki for your team, if working with others. For example, note that all new code must run through rustfmt and abide by Rust’s naming style.
	3.	Coding Phase: Implement features adhering to the best practices:
	•	Write code in small modules, using clear names. Break up functions that get too large or do too much.
	•	Use Result for any function that can fail; propagate errors with ?. Only use panic! for non-recoverable situations (and consider marking those functions as #[must_use] or documenting the panic condition).
	•	At this stage, run Clippy often (cargo clippy) to get feedback on your code. If Clippy suggests a more idiomatic approach (like using map instead of a manual match on Option), apply it to improve code quality.
	•	Keep performance in mind but write the straightforward implementation first. Trust the compiler optimizations – do not prematurely micro-optimize. If a function is very hot, you might mark it with #[inline], but consider doing so after profiling.
	•	Use threads or async as needed: if a feature requires concurrency (say, handling multiple requests), decide between threads or async based on the problem (few long-running tasks might use threads; many I/O-bound tasks likely async). Set up the basic concurrency using std::thread or Tokio early to ensure the design works.
	4.	Documentation as You Go: For every public item, add rustdoc comments. It’s often easier to write docs while the context is fresh. Include examples in the docs for new APIs. If you find it hard to explain an API clearly, it might be a sign to refactor or simplify the design. Run cargo doc --open occasionally to preview the documentation for readability.
	5.	Writing Tests: Develop tests alongside code. Write unit tests in the same file for the module’s functionality (e.g., in src/utils.rs, include #[cfg(test)] mod tests { ... }). Aim for thorough coverage of edge cases. If fixing a bug, add a test that would have caught that bug. Use integration tests (tests/ directory) for cross-module or high-level testing, treating the library as a black box. For example, if building a library, write a test in tests/usage.rs that uses the public API as an end-user would. If building a binary, consider integration tests that run the binary with certain inputs (you can use assert_cmd crate to execute your CLI in tests).
	6.	Performance Testing: If your project has performance requirements, set up a benchmark suite (using Criterion, perhaps in a benches/ directory) to measure critical operations. Do this before optimizing heavily – it provides a baseline. Run the benchmarks and use a profiler to identify bottlenecks if the project is non-trivial. Only apply optimizations where the profiler shows true hot spots. After changes, run the benchmarks again to verify improvements (and that you haven’t regressed elsewhere).
	7.	Security Checks: Audit usage of unsafe (if any). Each unsafe should be justified with a comment about why it’s safe. Run Miri (cargo +nightly miri test) on test suites to catch some classes of undefined behavior in unsafe code. Also run cargo audit to scan for vulnerable dependencies. If any are reported, update those dependencies or apply mitigations. This step ensures you haven’t introduced a known security hole via dependencies.
	8.	Prepare for Code Review: Before pushing code for review or merging:
	•	Run cargo fmt to auto-format the code.
	•	Run cargo clippy -- -D warnings to ensure no Clippy warnings remain. Address any suggestions or explicitly suppress false positives with #[allow(...)] (and document why).
	•	Run the full test suite (cargo test for all unit, integration, and doc tests) and ensure all pass.
	•	Run cargo doc to ensure documentation builds without warnings. Optionally, run cargo deadlinks (a tool to find broken links in docs).
	•	Double-check that public APIs have documentation and that all new pub items are intentional (sometimes one might accidentally leave something public).
	9.	Code Review Process: During review, discuss any deviations from these best practices. For example, if a function returns Result but could have returned a more specific error type, consider using thiserror to create a detailed error enum. If a certain piece of code is complex, consider if it can be broken down or if comments can clarify it. Ensure that any introduced unsafe code is scrutinized by multiple people if possible. Use the review to verify that all new code is covered by tests and that documentation is sufficient.
	10.	Continuous Integration and Merging: The CI will run on your pull request to validate formatting, linting, testing, etc. If the CI finds issues (like formatting differences or a Clippy lint), fix them. Once CI passes and reviews are approved, merge the code. On merge, consider having the CI run a final cargo audit and maybe deploy steps (if you release on merge to main or similar).
	11.	Release/Deployment: When it’s time to cut a release, update the version in Cargo.toml according to semver (if a library) or prepare a changelog. Ensure the CI (or local tests) pass on the commit that you plan to tag. For libraries, you might want to test that your crate works on the minimum supported Rust version by using rustup to run tests on that toolchain. Then publish via cargo publish. For applications, you might create a tag and let CI build binaries for distribution.
	12.	Post-merge Practices: Continuously monitor the project’s health. Set up Dependabot (on GitHub) or a similar service to automatically open PRs for dependency updates and run CI on them. This helps keep dependencies fresh. Also, keep an eye on Clippy and Rust compiler updates – new versions might add lints or warnings; fix those as they come (e.g., in Rust 1.60 a new warning might appear, update code accordingly). Make code maintenance a habit to avoid a large backlog of fixes later.

By following this workflow, you integrate best practices at every stage: planning (style and structure), coding (idioms, safe patterns), testing (ensuring reliability), reviewing (quality control), and releasing (deployment and maintenance). It embeds quality assurance (via CI) and continuous improvement (via Clippy, audit, etc.) into the development cycle, leading to a robust and idiomatic Rust codebase.

Examples

Below are some small examples illustrating these best practices:
	•	Error Handling Example: Using thiserror and anyhow.
Suppose we have a function that reads a configuration file and parses it. We’ll create a custom error type for our library using thiserror, and in our main application we’ll use anyhow to simplify error handling.

use thiserror::Error;
use std::io;
use std::fs;
use std::num::ParseIntError;

#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("I/O error while reading config: {0}")]
    Io(#[from] io::Error),
    #[error("Invalid number in config: {0}")]
    Parse(#[from] ParseIntError),
    #[error("Missing required field: {0}")]
    MissingField(String),
}

pub fn read_config(path: &str) -> Result<Config, ConfigError> {
    let text = fs::read_to_string(path)?;
    let config = parse_config_text(&text)?;
    Ok(config)
}
# // Assume Config and parse_config_text are defined elsewhere
# pub struct Config { pub threshold: u32 }
# fn parse_config_text(s: &str) -> Result<Config, ParseIntError> {
#     // trivial parse for example
#     let num: u32 = s.trim().parse()?;
#     Ok(Config { threshold: num })
# }

In this library code, we define ConfigError with variants for different error kinds, using #[from] to automatically convert io::Error and ParseIntError when using ?. We also provide a custom variant for a logical error (MissingField). Each variant has a user-friendly error message via the #[error("...")] attribute.
Now, in the application binary using this library:

use anyhow::{Context, Result};
use my_config_lib::read_config;

fn main() -> Result<()> {
    // Use anyhow::Result for main
    let config_path = "config.txt";
    let config = read_config(config_path)
        .with_context(|| format!("Failed to load configuration from {}", config_path))?;
    // If we reach here, config is successfully read.
    println!("Threshold value is {}", config.threshold);
    Ok(())
}

Here, we use anyhow::Result as the return type of main, so we can use ? on our library function which returns our custom ConfigError. We attach context using with_context to add a high-level description in case read_config fails. If any error occurs, this will print a message like:

Error: Failed to load configuration from config.txt
Caused by: Invalid number in config: ParseIntError { .. }

which is informative to the user. We didn’t have to write a lot of boilerplate to map errors; thiserror and anyhow handled it.

	•	Formatting and Clippy Example: Ensuring style and idioms.
Imagine a function that calculates the sum of squares of even numbers in a list. A newcomer to Rust might write:

fn sum_of_squares_evens(v: &[i32]) -> i32 {
    let mut sum = 0;
    for x in v {
        if x % 2 == 0 {
            sum += x * x;
        }
    }
    return sum;
}

While this is functionally correct, Clippy would suggest some improvements. Clippy might warn that the return is redundant (Rust implicitly returns the last expression). It might also suggest using iterators for clarity. An idiomatic refactor could be:

fn sum_of_squares_evens(v: &[i32]) -> i32 {
    v.iter()
     .filter(|&x| x % 2 == 0)
     .map(|x| x * x)
     .sum()
}

This version is succinct and conveys intent clearly. It leverages iterator adapters to filter evens and map to squares, then sum them. It has no explicit mutable variable or loop, reducing chances for mistakes. We rely on Rust’s zero-cost abstraction for iterators, confident this will be optimized well. We also removed the explicit return in favor of the expression result, which is the idiomatic style. Running cargo fmt would ensure consistent indentation and alignment, and running cargo clippy would ensure we caught any other lint (for example, Clippy’s needless_return or clippy::style lints would have guided us here).

	•	Concurrency Example: Using threads with channels.
Suppose we want to parallelize processing of a vector of data. We’ll spawn worker threads and collect results via a channel:

use std::thread;
use std::sync::mpsc;

fn process_data_concurrently(data: Vec<i32>) -> i32 {
    let (tx, rx) = mpsc::channel();
    let chunks: Vec<_> = data.chunks(100).collect();
    for chunk in chunks {
        let tx_clone = tx.clone();
        // clone the data we need for thread
        let chunk_vec = chunk.to_owned();
        thread::spawn(move || {
            let partial_sum: i32 = chunk_vec.iter().map(|x| x * 2).sum();
            // send the result back
            tx_clone.send(partial_sum).expect("Failed to send");
        });
    }
    drop(tx); // close the original sender so the channel will end eventually
    // collect results
    let mut total = 0;
    for partial in rx {  // will iterate until channel closes
        total += partial;
    }
    total
}

In this example, we divide the data into chunks of 100 and spawn a thread for each chunk to do some computation (doubling each number and summing, just as an example workload). We use an MPSC (multi-producer, single-consumer) channel to collect the partial sums. We clone the transmitter for each thread and move it into the thread. In the end, we drop(tx) in the main thread to indicate no more messages will be sent (so that the loop for partial in rx will end when all worker threads are done). This design avoids shared mutable state by communicating via the channel. It’s a classic example of message-passing concurrency in Rust. The code uses only thread-safe constructs (mpsc and threads), and the compiler ensures everything we moved into the thread is Send (for example, if chunk_vec weren’t Send, the thread::spawn would not compile).
With proper error handling (in this simple snippet we expect on send, which is fine here since failing to send means the receiver was dropped unexpectedly), this approach is scalable. If this was an async scenario, we would use tokio::spawn and perhaps tokio::sync::mpsc for channel, but the idea of isolating tasks and using message passing holds similarly.

	•	Unsafe Code Example: Safe abstraction for an FFI.
Consider you need to call a C library function that fills a buffer. The C function is:

// C library
// Fills `buffer` with `n` random bytes. Returns 0 on success.
int get_random_bytes(uint8_t *buffer, size_t n);

We want to provide a safe Rust wrapper:

use std::ffi::c_void;
extern "C" {
    fn get_random_bytes(buffer: *mut c_void, n: usize) -> i32;
}

pub fn fill_random(buffer: &mut [u8]) -> Result<(), &'static str> {
    let ret = unsafe { 
        get_random_bytes(buffer.as_mut_ptr() as *mut c_void, buffer.len()) 
    };
    if ret == 0 {
        Ok(())
    } else {
        Err("get_random_bytes failed")
    }
}

Here, the unsafe block is used only to call the foreign function. We ensure safety by:
	•	Converting Rust’s &mut [u8] slice into a raw pointer and length, which is safe as long as we pass them correctly. We use buffer.as_mut_ptr() which gives *mut u8 pointing to the slice’s memory, and cast it to *mut c_void since that’s what the C function expects.
	•	We know buffer.as_mut_ptr() and buffer.len() represent a valid chunk of memory (because &mut [u8] guarantees exclusive access and correct length).
	•	After the call, we check the return value and convert it into a Rust Result. We encapsulate all this in a safe function fill_random. The unsafe details (FFI call, pointer cast) are not exposed to the user of fill_random.
In documentation, we’d note: “Fills the buffer with random bytes from the C library. Safety: This function assumes the underlying C function is safe to call with the provided buffer. The buffer must be valid for writes of the given length.” Actually, because we take &mut [u8], the safety requirement is already enforced by Rust’s type system (the slice is guaranteed valid). So fill_random is a fully safe function to call.
This demonstrates how to use unsafe in a minimal way and build a safe abstraction over it. Anyone using fill_random in Rust can do so like any other safe function, without worrying about raw pointers or C invariants.

	•	Testing and Documentation Example: Doc-test with error section.
Suppose we have a function that parses an integer percentage from a string:

/// Parses a percentage string (e.g. "42%") into a number (0-100).
///
/// # Errors
/// Returns an error if the input is not in the format "<number>%"
/// or if the number is not between 0 and 100.
///
/// # Examples
/// ```
/// # use mycrate::parse_percentage;
/// let val = parse_percentage("75%").unwrap();
/// assert_eq!(val, 75);
/// assert!(parse_percentage("110%").is_err());
/// ```
pub fn parse_percentage(input: &str) -> Result<u8, String> {
    if let Some(stripped) = input.strip_suffix('%') {
        let num = stripped.parse::<u8>().map_err(|e| e.to_string())?;
        if num <= 100 {
            Ok(num)
        } else {
            Err("Percentage out of range".into())
        }
    } else {
        Err("Missing '%' sign".into())
    }
}

This example shows a doc comment with:
	•	a clear description,
	•	an Errors section explaining when an error is returned,
	•	examples that will be tested (including an error case and a normal case).
Running cargo test will compile the example in the doc comment to ensure that parse_percentage("110%").is_err() indeed is true, and that parse_percentage("75%") returns 75. This ensures our documentation is accurate and our function behaves as documented. If we change the code in a way that breaks the example (or if the example is wrong), the doc test will fail, prompting us to update either the code or the docs so they remain in sync.

	•	Continuous Integration YAML (GitHub Actions) Example: Finally, to tie several practices together, here is a snippet of a GitHub Actions workflow (YAML) that a project might use to enforce these best practices on each push:

name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        rust: [1.72.0, stable]  # test on MSRV 1.72 and latest stable
    steps:
    - uses: actions/checkout@v3
    - uses: actions-rs/toolchain@v1
      with:
        toolchain: ${{ matrix.rust }}
        components: rustfmt, clippy
        override: true
    - name: Build
      run: cargo build --all --verbose
    - name: Run Clippy
      run: cargo clippy --all-targets --all-features -- -D warnings
    - name: Run Tests
      run: cargo test --all --verbose
    - name: Check Formatting
      run: cargo fmt -- --check
    - name: Audit dependencies
      run: cargo install cargo-audit && cargo audit

This CI script:
	•	Checks out code, sets up Rust toolchain,
	•	Builds the project (all targets and features, which is good for catching any optional feature issues),
	•	Runs Clippy with -D warnings to fail on any warnings (ensuring lint cleanliness),
	•	Runs tests,
	•	Checks formatting,
	•	Audits dependencies for vulnerabilities.
With such a pipeline, every push or PR must pass these checks. This guarantees that code adheres to style (rustfmt), is linted (Clippy), is tested, and that you don’t accidentally introduce known vulnerable crates. It automates the enforcement of many best practices discussed in this guide.

These examples demonstrate in a concrete way how to implement Rust best practices in real code. They cover proper error handling patterns, writing idiomatic and performant code, using concurrency safely, encapsulating unsafe, writing thorough documentation and tests, and setting up CI to enforce standards. Each piece helps ensure the overall quality and reliability of a Rust codebase.

References
	•	The Rust Programming Language Book (2018 edition & later) – The official book covers Rust basics and conventions, including chapters on Error Handling, Testing, and Concurrency. Available on doc.rust-lang.org/book.
	•	Rust Style Guide and rustfmt – Official Rust style guide describing formatting and naming conventions, and the rustfmt tool which automatically formats code. See doc.rust-lang.org/style-guide and the rustfmt README.
	•	Rust API Guidelines – Document detailing best practices for Rust library design, naming, documentation, and more. Especially relevant for naming conventions and documenting public APIs. Hosted at rust-lang.github.io/api-guidelines.
	•	Cargo Book (The Cargo Reference) – Official guide to Cargo, including features and dependency management and workspaces. See doc.rust-lang.org/cargo, particularly the “Features” chapter.
	•	thiserror and anyhow crates – Documentation for the thiserror derive macro crate and anyhow library for error handling. See docs.rs for each (e.g., anyhow crate docs which demonstrate usage, and thiserror README for examples).
	•	Clippy (Rust Linting Tool) – Official Rust linter. Documentation at doc.rust-lang.org/clippy about usage and configurable lints. Its GitHub README and lint list are useful to understand what patterns Clippy checks (for example, discouraging unwrap() in examples).
	•	Rustonomicon (The Dark Arts of Unsafe Rust) – Guide for writing unsafe code correctly. Covers the principles of unsafe, examples of common patterns, and the contracts one must follow. Available at doc.rust-lang.org/nomicon.
	•	Rust Async Book – Asynchronous Programming in Rust is an official book detailing how to write async code, choose executors, etc., useful for understanding futures, async/await, and common pitfalls (like not blocking inside async). See rust-lang.github.io/async-book.
	•	The Rust Performance Book by Nicholas Nethercote – An online book detailing tips for optimizing Rust programs (covering inlining, memory, etc.). Although not an official rust-lang publication, it’s a well-respected resource for performance tuning: nnethercote.github.io/perf-book/.
	•	Rust Secure Code Working Group / Cargo Audit – The RustSec advisory database and cargo-audit tool. Reference: RustSec’s Advisory DB on GitHub and the cargo audit tool documentation.
	•	CI Configuration Examples – Many open-source projects and guides (like Zero to Production in Rust by Luca Palmieri) provide examples of setting up CI with fmt, Clippy, tests, and audit. See the GitHub Actions snippet above or resources like the actions-rs documentation.
	•	Rust Community Style Guidelines – Various blog posts and community docs (e.g., the “Rust Coding Conventions” or Microsoft’s Rust guidelines) summarize idiomatic practices. While unofficial, they often align with the Rust API guidelines and Clippy lints.
	•	Official Rust Blog – The Rust blog (blog.rust-lang.org) sometimes has posts on best practices or announcements (like the lockfile guidance change). These can provide rationale for certain practices (e.g., committing Cargo.lock).

Each of these resources can provide deeper insight and rationale for the guidelines summarized in this guide. For new Rustaceans, starting with the official book and then referring to the API guidelines and Clippy docs will build a strong foundation. Experienced developers will find the Performance book, Async book, and Rustonomicon helpful for advanced topics. Keeping the references handy will help ensure that your Rust development aligns with the broader Rust community’s established best practices.

Overview

This skill is a compact, practical guide to modern Rust best practices covering style, error handling, performance, concurrency, project layout, dependency management, documentation, testing, security, and CI. It gives actionable conventions and patterns to write idiomatic, maintainable, and performant Rust code. Use it as a checklist or reference during development, reviews, and CI setup.

How this skill works

The guide inspects common Rust development areas and prescribes concrete rules: naming conventions, rustfmt usage, module and workspace organization, error handling patterns (Result vs panic, custom errors, anyhow/thiserror), performance techniques, safe concurrency, and async best practices. It explains what to prefer, when to use specific crates/traits, and how to structure projects and CI checks to enforce consistency. Practical examples and tooling recommendations (Cargo, Tokio, Criterion, rustfmt, linters) are provided to apply the rules in real projects.

When to use it

  • Starting a new Rust project or reorganizing an existing crate
  • Writing library APIs vs applications (error handling decisions)
  • Optimizing hotspots after profiling or benchmarking
  • Designing concurrent or async systems (thread vs async choices)
  • Setting up CI checks for formatting, linting, and tests

Best practices

  • Follow Rust naming idioms: UpperCamelCase for types, snake_case for functions/modules, SCREAMING_SNAKE_CASE for constants
  • Run rustfmt and enforce with CI (cargo fmt -- --check); keep lines under 100 chars
  • Return Result<T, E> for recoverable errors; reserve panic! for invariant violations
  • Use thiserror for library error enums and anyhow in application top-level for ergonomic error handling with context
  • Prefer high-level idiomatic code (iterators, ownership) and only optimize after profiling with Criterion and profilers
  • Favor message passing, Arc/Mutex or async-aware primitives; avoid blocking inside async functions and use spawn_blocking when needed

Example use cases

  • Library author defining a typed Error enum with thiserror and documenting public APIs
  • Web server using Tokio: use async tasks, tokio::sync primitives, and ensure spawned futures are Send when using the multi-threaded runtime
  • CLI application returning Result<(), anyhow::Error> from main and adding context() to I/O operations
  • Performance tuning: profile with cargo flamegraph then replace a LinkedList with Vec or add Vec::with_capacity where allocations dominate
  • CI pipeline that runs cargo fmt -- --check, cargo clippy -- -D warnings, and unit/integration tests

FAQ

When should I use anyhow vs thiserror?

Use thiserror for libraries so callers can match on error variants; use anyhow in application top-level code for convenience and to attach context while propagating diverse error types.

How do I avoid blocking the async runtime?

Never run blocking I/O or heavy CPU work directly in async functions; offload to spawn_blocking or a dedicated thread pool and use async-aware synchronization primitives like tokio::sync::Mutex.