Introduction
Code formatting and linting are often treated as nice-to-have luxuries, but they're actually foundational to shipping reliable Rust. We've seen teams waste countless hours in code review debates about spacing, or miss real bugs hiding in warnings we all learned to ignore. This guide covers Clippy and rustfmt: a practical guide for developers who want to eliminate these friction points and keep codebases clean automatically.
Rust's tooling ecosystem gives us two remarkable tools out of the box. rustfmt handles formatting consistently across the entire team, and Clippy catches logical errors, performance problems, and idiomatic violations before they reach production. When wired into CI properly, they become invisible guardrails that improve code quality without requiring a single code review comment about whitespace.
rustfmt — Consistent formatting without debate
rustfmt is Rust's official code formatter. Running cargo fmt rewrites your code to match a consistent style, and crucially, it's opinionated enough that there's almost nothing to debate. This alone saves time and mental energy across the team.
Most projects ship with rustfmt's defaults and never need to customize. That's intentional—the Rust team chose sensible defaults that work for the vast majority of codebases. In a microservices environment, we've standardized on rustfmt across all services without a single configuration file, and that consistency makes code reviews focus on logic, not tabs versus spaces.
To format your entire workspace:
cargo fmtTo check formatting without modifying files (useful in CI):
cargo fmt -- --checkFor a real example, consider this slightly messy code:
fn process_order(id: u64, items: Vec<Item>) -> Result<Order, OrderError> {
let total = items.iter().map(|i| i.price).sum::<f64>();
let discount = if total > 100.0 { 0.1 } else { 0.0 };
let final_price=total*(1.0-discount);
Ok(Order { id, total: final_price, items })
}After cargo fmt:
fn process_order(id: u64, items: Vec<Item>) -> Result<Order, OrderError> {
let total = items.iter().map(|i| i.price).sum::<f64>();
let discount = if total > 100.0 { 0.1 } else { 0.0 };
let final_price = total * (1.0 - discount);
Ok(Order { id, total: final_price, items })
}Spacing is consistent, indentation is correct, and operators have breathing room. Nobody on the team needs to think about this anymore. If you customize rustfmt at all, create a rustfmt.toml at the workspace root with minimal overrides:
edition = "2021"
max_width = 100
hard_tabs = false
tab_spaces = 4
Most teams we work with never change these values. The defaults work well for production systems handling high throughput and complex domain logic.
Clippy — Catching real bugs and style issues
Clippy is a linter that catches common mistakes, performance anti-patterns, and idiomatic violations. It's more aggressive than rustc's own warnings, and it often prevents real bugs from reaching production. Running cargo clippy should be part of your standard development loop, right after cargo test.
Clippy comes with lint groups organized by severity: correctness, performance, complexity, style, and others. Most of these are enabled by default, but you can be stricter if you want:
cargo clippy -- -D warningsThis treats all warnings (including Clippy's) as errors, which is exactly what you want in CI.
Here's a real scenario from an e-commerce service: a developer wrote this code to fetch products:
pub fn get_product_prices(products: &[Product]) -> Vec<f64> {
let mut prices = Vec::new();
for p in products {
prices.push(p.price);
}
prices
}Running cargo clippy immediately flags this:
warning: this loop could be written as a call to `map`
--> src/lib.rs:2:5
|
2 | for p in products {
3 | prices.push(p.price);
|
= help: ...or consider collecting into a `Vec` and using `extend`
warning: <generic> error: you are creating a `Vec` and immediately pushing into itThe idiomatic Rust version is cleaner and potentially more efficient:
pub fn get_product_prices(products: &[Product]) -> Vec<f64> {
products.iter().map(|p| p.price).collect()
}Clippy catches dozens of patterns like this: unnecessary allocations, inefficient string operations, redundant closures, clone-and-drop patterns, and logic errors. In one CRM backend, Clippy found that we were cloning a database connection pool unnecessarily in every middleware layer—a performance win that code review alone might have missed.
Run Clippy regularly during development:
cargo clippy --all-targets --all-featuresThe --all-targets flag checks tests and examples too, catching issues that might only surface in those contexts. --all-features ensures you're checking all code paths, which matters in libraries with multiple optional features.
CI integration — Fail builds on warnings
Formatting and linting are most powerful when they're enforced in CI, not left as suggestions. Many teams we've worked with saw a dramatic improvement in code quality the moment they started failing CI builds on warnings.
Here's a typical GitHub Actions workflow:
name: Lint and Format
on: [push, pull_request]
jobs:
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- run: cargo fmt -- --check
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- run: cargo clippy --all-targets --all-features -- -D warnings
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- run: cargo testThe key line is cargo fmt -- --check, which returns a non-zero exit code if any file is misformatted. Similarly, -- -D warnings converts all warnings into hard errors. When a developer pushes code that doesn't pass these checks, CI blocks the merge until they run cargo fmt locally and push again.
This approach eliminates the most tedious part of code review. Nobody has to comment "please add a space before this brace" ever again. The machine handles it, and developers fix it in seconds.
In a trading system with strict latency requirements, we also added performance lints to catch allocations and unnecessary moves in hot paths:
cargo clippy --all-targets --all-features -- \
-D warnings \
-D clippy::perf \
-D clippy::correctnessThis ensures that performance anti-patterns are caught before code review, letting humans focus on algorithmic correctness and business logic.
Configuring Clippy — Allow justified exceptions
Not every Clippy warning applies to every codebase. Sometimes you have valid reasons to deviate from a lint rule, and Rust gives you tools to document those exceptions explicitly.
The most common approach is to allow a specific lint at the module or function level using #[allow(...)]:
#[allow(clippy::too_many_arguments)]
fn create_user(
first_name: String,
last_name: String,
email: String,
phone: String,
address: String,
city: String,
country: String,
) -> User {
// implementation
}We added this #[allow(...)] because the business logic genuinely requires these seven parameters; we can't refactor them into a struct without breaking the API contract with other teams. The exception is documented right where it matters.
For more complex scenarios, create a .clippy.toml in your workspace root:
too-many-arguments-threshold = 10
doc-markdown-code-line-threshold = 100
You can also define a custom lint profile using attributes. If your team has specific style preferences that Clippy doesn't enforce, you can create your own lint rules, though this is rare and typically only necessary for very large organizations with strict standards.
In a ride-hailing backend, we disabled the clippy::result_unit_err lint globally because our domain model uses Result<T, ()> for signal-based error handling in certain contexts:
#![allow(clippy::result_unit_err)]This goes at the top of lib.rs and applies to the entire crate. Always add a comment explaining why:
// Our service uses Result<T, ()> for quick signal propagation in retry loops.
// The overhead of a full Error type isn't justified for these hot paths.
#![allow(clippy::result_unit_err)]The key principle: exceptions should be explicit, justified, and documented. A developer reading the code two years later should understand why this lint was disabled.
Common mistakes
Ignoring Clippy suggestions in development. Some developers turn off warnings or just ignore them. This defeats the purpose. Clippy is your teammate; it's making genuine suggestions. Make it part of your regular workflow, like running tests.
Not checking formatting in CI. If cargo fmt -- --check isn't in your CI pipeline, you'll lose consistency as the team grows. One developer prefers four-space indents, another uses tabs; soon the codebase is a mess.
Allowing too many exceptions without justification. If you have dozens of #[allow(...)] attributes scattered throughout your code, you've essentially disabled the linter. Step back and ask: is this Clippy rule genuinely wrong for your domain, or are we just avoiding refactoring?
Using outdated Rust toolchain in CI. Clippy adds new lints regularly, and old versions might miss issues. Update your Rust toolchain in CI at least monthly. Using a pinned stable version (like dtolnay/rust-toolchain@stable) keeps you current without chasing nightlies.
Key takeaways
- rustfmt removes formatting debates entirely. Run
cargo fmtbefore every commit andcargo fmt -- --checkin CI. Use defaults unless you have a specific reason not to. - Clippy catches real bugs and idiom violations. Make
cargo clippy -- -D warningspart of your standard development loop and CI pipeline. It's not pedantry; it's practical error prevention. - Fail CI builds on formatting and lint warnings. This forces the team to maintain standards without code review friction. Developers fix issues in seconds locally before the build even reaches the repo.
- Document exceptions explicitly. When you disable a lint with
#[allow(...)], explain why. Future maintainers should understand the reasoning immediately. - Treat tooling as part of your development culture. Teams that embrace rustfmt and Clippy ship cleaner code, spend less time in review, and catch more bugs early. It's not extra work; it's work multiplication.






Comments
Loading comments…