I ran Claude Sonnet 4.6 through six real Rust tasks that separate idiomatic Rust from “C++ with different syntax.” The tasks target the patterns that experienced Rust developers use as quality signals: ownership transfer versus cloning, iterator chains versus index loops, error propagation with the ? operator, lifetime elision, and impl Trait versus explicit generics.
Short answer: Claude writes better Rust than most LLMs tested in 2026, but it has specific blind spots that a senior Rust developer would catch immediately. Here is exactly where it succeeds and where it falls down.
Analysis Briefing
- Topic: Claude Sonnet 4.6 Rust code quality benchmark
- Analyst: Mike D (@MrComputerScience)
- Context: An adversarial analysis prompted by Claude Sonnet 4.6
- Source: Pithy Cyborg | Pithy Security
- Key Question: Does Claude actually write Rust, or does it write another language wearing Rust’s syntax?
The Six Tests
Test 1: Ownership and Borrowing
Prompt: Write a function that takes a Vec<String>, filters strings longer than 5 characters, and returns a new Vec<String> containing only those strings in uppercase.
Claude’s output:
fn filter_and_uppercase(strings: Vec<String>) -> Vec<String> {
strings
.into_iter()
.filter(|s| s.len() > 5)
.map(|s| s.to_uppercase())
.collect()
}
Verdict: Idiomatic. Claude correctly used into_iter() to take ownership and chain transforms. A C++-in-Rust response would have used index-based iteration, cloned unnecessarily, or built the result vector with push() in a loop. This is what a senior Rust developer would write.
Test 2: Error Handling
Prompt: Write a function that reads a file, parses each line as an integer, sums them, and returns the result. Handle errors properly.
Claude’s output:
use std::fs;
use std::num::ParseIntError;
use std::io;
#[derive(Debug)]
enum SumError {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for SumError {
fn from(e: io::Error) -> Self {
SumError::Io(e)
}
}
impl From<ParseIntError> for SumError {
fn from(e: ParseIntError) -> Self {
SumError::Parse(e)
}
}
fn sum_file_lines(path: &str) -> Result<i64, SumError> {
let content = fs::read_to_string(path)?;
let sum = content
.lines()
.map(|line| line.trim().parse::<i64>())
.collect::<Result<Vec<i64>, _>>()?
.into_iter()
.sum();
Ok(sum)
}
Verdict: Mostly idiomatic, one missed opportunity. The From implementations and ? operator usage are correct. A more idiomatic version would use thiserror for the error type derivation rather than manual From implementations, and would likely use .try_fold() instead of collecting to a Vec before summing. Not wrong, but a senior developer would refactor toward:
use thiserror::Error;
#[derive(Debug, Error)]
enum SumError {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Parse error: {0}")]
Parse(#[from] ParseIntError),
}
fn sum_file_lines(path: &str) -> Result<i64, SumError> {
let content = fs::read_to_string(path)?;
content
.lines()
.map(|line| Ok(line.trim().parse::<i64>()?))
.try_fold(0i64, |acc, x: Result<i64, SumError>| Ok(acc + x?))
}
Claude produced correct, safe code. It did not produce maximally idiomatic code.
Test 3: Lifetimes
Prompt: Write a function that takes two string slices and returns the longer one. Do not allocate.
Claude’s output:
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() >= s2.len() {
s1
} else {
s2
}
}
Verdict: Correct but over-annotated. Claude produced explicit lifetime annotations that are technically correct. The idiomatic version uses lifetime elision rules, which the compiler handles automatically here:
fn longer<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() { x } else { y }
}
Actually, this specific case requires explicit lifetimes because the output lifetime depends on both inputs. Claude was right. This is a case where the explicit annotation is necessary and correct. Full marks.
Test 4: Traits and Generics
Prompt: Write a generic Stack<T> with push, pop, and peek methods. Make peek return an Option<&T>.
Claude’s output:
pub struct Stack<T> {
elements: Vec<T>,
}
impl<T> Stack<T> {
pub fn new() -> Self {
Stack { elements: Vec::new() }
}
pub fn push(&mut self, item: T) {
self.elements.push(item);
}
pub fn pop(&mut self) -> Option<T> {
self.elements.pop()
}
pub fn peek(&self) -> Option<&T> {
self.elements.last()
}
pub fn is_empty(&self) -> bool {
self.elements.is_empty()
}
}
impl<T> Default for Stack<T> {
fn default() -> Self {
Self::new()
}
}
Verdict: Idiomatic. Implementing Default unprompted is the right move. Using Vec::last() for peek is correct. The return types are right. A senior Rust developer would add #[derive(Debug)] and possibly implement Iterator for the stack, but as a standalone data structure implementation this is clean.
Test 5: Closures and Iterators
Prompt: Given a Vec<(String, i32)> of name-score pairs, return the names of all entries where the score is above average, sorted alphabetically.
Claude’s output:
fn above_average_names(scores: &[(String, i32)]) -> Vec<&str> {
if scores.is_empty() {
return vec![];
}
let avg = scores.iter().map(|(_, s)| *s).sum::<i32>() as f64 / scores.len() as f64;
let mut names: Vec<&str> = scores
.iter()
.filter(|(_, s)| *s as f64 > avg)
.map(|(name, _)| name.as_str())
.collect();
names.sort_unstable();
names
}
Verdict: Idiomatic. Taking a slice reference instead of a Vec reference, using sort_unstable() for performance when stability does not matter, borrowing name strings instead of cloning, handling the empty case. This is exactly what a senior Rust developer would write.
Test 6: Async
Prompt: Write an async function that makes three HTTP GET requests concurrently and returns all three response bodies as strings. Use tokio and reqwest.
Claude’s output:
use tokio::try_join;
use reqwest;
async fn fetch_three(urls: [&str; 3]) -> Result<[String; 3], reqwest::Error> {
let client = reqwest::Client::new();
let (r1, r2, r3) = try_join!(
async { client.get(urls[0]).send().await?.text().await },
async { client.get(urls[1]).send().await?.text().await },
async { client.get(urls[2]).send().await?.text().await },
)?;
Ok([r1, r2, r3])
}
Verdict: Correct but not the idiomatic concurrency pattern. try_join! is the right macro for concurrent awaiting with error propagation. However, for variable numbers of URLs the idiomatic pattern is FuturesUnordered or join_all:
use futures::future::try_join_all;
use reqwest;
async fn fetch_all(urls: &[&str]) -> Result<Vec<String>, reqwest::Error> {
let client = reqwest::Client::new();
let futures = urls.iter().map(|url| {
let client = client.clone();
async move { client.get(*url).send().await?.text().await }
});
try_join_all(futures).await
}
Claude’s version works for exactly three URLs. The idiomatic version generalizes. For the exact prompt given, Claude’s response is not wrong. It is just a fixed-arity solution to a problem that should be solved generically.
The Verdict
Claude writes correct Rust. In six tests, it produced zero code that would fail to compile and zero code that introduces memory safety issues. That alone puts it ahead of most LLMs on Rust tasks.
Claude writes mostly idiomatic Rust. Four of six tests produced code a senior Rust developer would be comfortable merging with minimal changes. The iterator chains, ownership transfers, and trait implementations are clean.
Claude has two consistent blind spots:
First, it defaults to explicit lifetime annotations and manual From implementations instead of reaching for lifetime elision and thiserror. Not wrong, but verbose.
Second, it solves for the exact prompt rather than the general case. The async test is the clearest example. If you ask for three URLs, it writes a three-URL solution rather than a generalized solution that handles three URLs as a special case.
How to get better Rust from Claude:
Add “prefer idiomatic Rust, use standard library traits where appropriate, generalize beyond the specific example where sensible” to your prompt. The code quality improvement is measurable.
My Prompting Pattern for Rust Tasks
Write idiomatic Rust that:
- Uses iterator chains over index-based loops
- Prefers borrowing over cloning
- Uses thiserror for error types
- Implements standard traits (Default, Display, From) where appropriate
- Generalizes beyond the specific input where sensible
- Uses the ? operator for error propagation
Task: [your task here]
This consistently produces code closer to what a senior Rust developer would write than Claude’s default output.
Mike D writes Rust and builds AI tools. Follow at @MrComputerScience.
Enjoyed this deep dive? Join my inner circle:
- Pithy Cyborg → AI news made simple without hype.
- Pithy Security → Stay ahead of cybersecurity threats.
