Why Rust Needs Specific AI Coding Rules
Rust's compiler is famously strict — the borrow checker, lifetime annotations, and ownership system catch entire classes of bugs at compile time. You might expect AI assistants to generate safe, idiomatic Rust by default. They don't.
The most common AI failures in Rust: excessive cloning to satisfy the borrow checker (works but defeats the performance purpose of Rust), using unwrap() everywhere instead of proper error propagation, generating overly complex lifetime annotations when restructuring would be simpler, fighting the ownership model with Rc<RefCell<T>> when a simpler architecture would work, and ignoring the type state pattern where it applies.
These aren't compilation errors — the code compiles and runs. But it's not the Rust your team wants to write. It's Rust written by an AI that learned from all languages and applies the path of least resistance, not the idiomatic path.
Rule 1: Idiomatic Ownership and Borrowing
The AI's favorite escape hatch in Rust is .clone(). Can't figure out the lifetime? Clone it. Borrow checker complaining? Clone it. Need the value in two places? Clone it. The result is Rust code that compiles perfectly but performs like a garbage-collected language.
The rule: 'Minimize .clone() usage. If you need to clone to satisfy the borrow checker, first consider restructuring the code to avoid the need. Use references (&T, &mut T) as function parameters instead of owned values where possible. Only use .clone() when the data genuinely needs to be independently owned by multiple values. Never clone inside a loop.'
For structs, add: 'Prefer borrowing over ownership in struct fields where the lifetime can be expressed simply. If a struct only needs to read data, take &str not String, &[T] not Vec<T>. Only own data when the struct needs to outlive the source.'
Excessive .clone() is the Rust equivalent of a code smell. If the AI clones to satisfy the borrow checker, restructure for references first. Never clone inside a loop — it defeats Rust's performance advantage.
Rule 2: Error Handling with thiserror and anyhow
AI assistants love .unwrap() in Rust because it's the shortest path from Result<T, E> to T. But unwrap() panics on error, which is almost never what you want in production code. The AI also generates inconsistent error types — some functions return String errors, others return Box<dyn Error>, and nothing composes cleanly.
The rule: 'Never use .unwrap() or .expect() in production code — only in tests and examples. Use the ? operator for error propagation. Define error types with thiserror for library code. Use anyhow::Result for application code. Map foreign errors with .map_err() or thiserror's #[from] attribute.'
For the binary vs library distinction: 'In lib.rs and library crates, define explicit error enums with #[derive(thiserror::Error)]. In main.rs and binary crates, use anyhow::Result<T> and anyhow::Context for adding context to errors.'
- Never .unwrap() in production — use ? operator for propagation
- thiserror for library crates — explicit, typed error enums
- anyhow for binary crates — flexible, contextual error chains
- #[from] attribute to convert between error types automatically
- .context() and .with_context() to add human-readable error messages
Simple rule: thiserror for library crates (explicit error types consumers can match on), anyhow for binary crates (flexible error chains with context). Never mix them in the same crate.
Rule 3: Trait Design and Implementation
AI assistants generate Rust traits that look like Java interfaces — large, kitchen-sink abstractions with 10+ methods. Idiomatic Rust prefers small, focused traits that compose. The Iterator trait has one required method. The Display trait has one method. Your traits should follow the same pattern.
The rule: 'Design traits with 1-3 required methods. Provide default implementations for derived behavior. Use trait bounds (impl Trait) for function parameters, not concrete types. Prefer static dispatch (generics with trait bounds) over dynamic dispatch (dyn Trait) unless you need heterogeneous collections or dynamic behavior at runtime.'
For common trait implementations: 'Derive standard traits where applicable: Debug on all public types, Clone and Copy on small value types, PartialEq/Eq and Hash on types used in collections, Serialize/Deserialize for types that cross API boundaries.'
Rule 4: Async Rust Patterns
Async Rust is where AI assistants struggle most. The interaction between async/await, the borrow checker, and trait objects creates complexity that AI models frequently resolve incorrectly — generating code that either doesn't compile or compiles but contains subtle concurrency bugs.
The rule: 'Use tokio as the async runtime (unless the project uses async-std). All async functions should return concrete types, not Box<dyn Future>. Use tokio::spawn for concurrent tasks and tokio::select! for racing futures. Never hold a MutexGuard across an .await point — restructure to drop the guard before awaiting.'
For async trait methods: 'Use the async-trait crate for async methods in traits. Be explicit about Send bounds: use #[async_trait] for Send futures (required for tokio::spawn), use #[async_trait(?Send)] only for single-threaded contexts.'
Never hold a MutexGuard across an .await point — it can cause deadlocks with tokio's work-stealing scheduler. Drop the guard before awaiting, or restructure to minimize the critical section.
Rule 5: Testing Patterns
Rust's built-in test framework is minimal compared to Jest or pytest. AI assistants often generate tests that under-utilize Rust's testing capabilities — writing only basic assert_eq! tests without using the module system, test attributes, or property-based testing.
The rule: 'Place unit tests in a #[cfg(test)] mod tests block at the bottom of each module. Use #[test] for synchronous tests, #[tokio::test] for async tests. Use assert_eq!, assert_ne!, and assert! with descriptive messages. For complex assertions, use the claims or assert_matches crate.'
For integration tests: 'Place integration tests in the tests/ directory. Each file is compiled as a separate crate. Use common fixtures through a tests/common/mod.rs file. Test public API surfaces only — integration tests shouldn't access private internals.'
- Unit tests in #[cfg(test)] mod tests at bottom of each module
- #[tokio::test] for async tests — not blocking on a runtime manually
- Descriptive assertion messages: assert_eq!(got, want, "user should be active")
- Integration tests in tests/ directory — test public API only
- Use proptest or quickcheck for property-based testing of algorithms
- Use mockall for mocking traits in unit tests
Complete Rust Rules Template
Here's a consolidated template for Rust teams. It covers ownership, error handling, traits, async, and testing — the five areas where AI output most frequently deviates from idiomatic Rust.
- Minimize .clone() — restructure for references before cloning. Never clone in loops.
- Never .unwrap() in production — use ? operator, thiserror (lib), anyhow (bin)
- Small traits (1-3 methods) — prefer static dispatch, derive Debug/Clone/PartialEq
- Tokio async runtime — no MutexGuard across .await, async-trait for trait methods
- Unit tests in #[cfg(test)] — integration tests in tests/ — proptest for properties
- Prefer enums over boolean flags — make invalid states unrepresentable
- Use type aliases for complex types — type Result<T> = std::result::Result<T, MyError>
- Clippy clean — run cargo clippy with -D warnings, fix all lints