Marc Love

Using MDX

Agentic coding tools are changing how we write software. Claude Code, Codex, Cursor, and their descendants have moved from novelty to daily driver for a growing number of developers. But anyone who has spent serious time with these tools knows their failure modes intimately. Agents hallucinate plausible-looking code that doesn’t actually work. They lose track of logic scattered across many files. They leave behind dead code and cruft during iteration. They struggle to reason about deep inheritance hierarchies and implicit behavior. And the errors surface late—at runtime or in production—after wasted cycles and frustrated debugging sessions.

These aren’t bugs in the tools. They’re fundamental limitations of the agentic approach: a language model generating code in a loop, checking results, and revising. The question isn’t whether agents make mistakes, but how quickly those mistakes get caught and corrected.

This is where Rust becomes interesting. The language’s design creates a tight feedback loop that directly addresses these weaknesses. The compiler acts as a collaborator, catching agent mistakes in seconds rather than minutes or days. If you’ve been using agentic tools but haven’t tried them with Rust, you’re likely leaving significant performance on the table.

The Feedback Loop: Why It Matters for Agents

Agentic tools operate in loops: generate code, check results, revise. This isn’t a limitation to work around—it’s the core mechanism. The quality of output depends heavily on how tight and fast this loop can run.

Consider what happens when an agent writes incorrect code. In a dynamically typed language, the code might look perfectly reasonable. It parses. It runs. Then it fails at runtime with a cryptic error, or worse, produces subtly wrong results that don’t surface until production. Each failed iteration costs time, tokens, and context. The agent has to reason backward from a runtime failure to the source of the problem, often without the full picture.

Rust’s compiler inverts this dynamic. Errors surface immediately, before anything runs. When an agent writes code that violates type constraints, ownership rules, or lifetime requirements, it learns about the problem in seconds. The feedback is specific and actionable. The agent can fix the issue and move forward rather than debugging a failure that happened three iterations ago.

This “fail fast” model changes what kinds of bugs remain after compilation. They’re mostly business logic bugs—the kind humans should review anyway. The mechanical errors that plague agentic development in more permissive languages simply don’t survive to runtime.

Catching Hallucinations Before They Escape

The hallucination problem is central to agentic coding. Agents generate code that looks correct but isn’t. Syntactically valid, stylistically plausible—but semantically wrong. A function call with arguments in the wrong order. An API method that doesn’t exist. A variable used before initialization.

In permissive languages, this code runs and fails later, or silently produces wrong results. Rust’s type system acts as a hallucination defense layer, catching these errors at compile time.

Strong Typing as Inline Documentation

Type signatures encode intent directly in the code. When an agent hallucinates a function call with wrong argument types, the compiler rejects it immediately. But more importantly, types act as inline documentation—the agent sees the contract without searching through documentation or guessing from examples.

Consider this function signature:

fn transfer_funds(
    from: &mut Account,
    to: &mut Account,
    amount: NonZeroU64,
) -> Result<TransactionId, InsufficientFunds>

The signature itself tells a story. The &mut indicates both accounts will be modified. NonZeroU64 means the amount can’t be zero—no need to check for that edge case. The Result return type makes explicit that this operation can fail, and exactly how. An agent reading this signature has enough information to use the function correctly without consulting any other documentation.

Option and Result: No Null Surprises

Rust encodes absence and failure in types rather than runtime exceptions. Option<T> means a value might not exist. Result<T, E> means an operation might fail. The compiler forces handling of these cases—agents can’t forget edge cases because the code won’t compile until they’re addressed.

Contrast this with null pointer exceptions, one of the most common runtime bugs across programming languages. An agent in a null-permissive language might write code that assumes a value exists when it doesn’t. That bug won’t surface until the specific code path executes in production. In Rust, the same logical error becomes a compile-time failure that the agent can fix immediately.

Ownership: Eliminating the Hard Bugs

Some bugs are hard to find because they require specific conditions to manifest. Shared mutable state and concurrency bugs are notoriously difficult to reproduce—they depend on timing, on which thread runs first, on memory layout that varies between runs. These bugs are hard for human developers. They’re even harder for agents, which lack the ability to reason about runtime behavior they can’t observe.

Rust’s ownership system makes these compile-time errors instead of runtime mysteries. An agent can’t accidentally introduce a data race because the compiler won’t allow it. The rules are strict—only one mutable reference at a time, or many immutable references—but they eliminate entire categories of bugs that would otherwise require sophisticated debugging to find.

Error Messages That Enable Self-Correction

Rust’s compiler errors often contain specific resolution hints. This isn’t an accident; it’s a design philosophy. The error messages are written to be actionable.

error[E0382]: borrow of moved value: `data`
  --> src/main.rs:15:20
   |
12 |     let data = vec![1, 2, 3];
   |         ---- move occurs because `data` has type `Vec<i32>`
13 |     process(data);
   |             ---- value moved here
14 |
15 |     println!("{:?}", data);
   |                      ^^^^ value borrowed here after move
   |
help: consider cloning the value if the performance cost is acceptable
   |
13 |     process(data.clone());
   |                 ++++++++

The error explains what happened (the value moved), where it happened (line 13), why it’s a problem (borrowing after move), and how to fix it (consider cloning). An agent can read this error, understand the issue, and apply a fix. Compare this to a segmentation fault or a null pointer exception, which tells you almost nothing about the source of the problem.

Keeping Logic Within Reach

Agents have limited context windows. Even as these windows grow, the fundamental constraint remains: an agent reasoning about code must hold that code in memory. Logic spread across many files means the agent must search, read, and maintain more code in context. Each file-hop costs time and risks losing track of the bigger picture.

Rust’s conventions work in favor of keeping logic accessible.

One Module, One File, Co-located Tests

Rust convention keeps implementation and unit tests in the same file. A module lives in one place, and its tests live right below it, typically in a #[cfg(test)] submodule. An agent working on a module has everything in one place—the implementation, the tests, the documentation.

pub struct Parser {
    // implementation details
}

impl Parser {
    pub fn parse(&self, input: &str) -> Result<Ast, ParseError> {
        // parsing logic
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_empty_input() {
        let parser = Parser::new();
        assert!(parser.parse("").is_err());
    }

    #[test]
    fn parse_valid_expression() {
        let parser = Parser::new();
        let result = parser.parse("1 + 2");
        assert!(result.is_ok());
    }
}

Contrast this with Java-style separation of src/ and test/ directories, where the agent must navigate between parallel directory structures to understand how code is tested and used.

Composition Over Inheritance

Rust doesn’t have inheritance in the traditional sense. Instead, it uses structs, traits, and enums—simple building blocks that compose explicitly. There are no deep inheritance hierarchies to trace through, no fragile base class problem where changing a parent class breaks distant children.

Consider how you’d model different message types in an object-oriented language versus Rust:

enum Message {
    Text { content: String, sender: UserId },
    Image { url: Url, dimensions: Size, sender: UserId },
    System { content: String },
}

fn process_message(msg: Message) {
    match msg {
        Message::Text { content, sender } => { /* handle text */ }
        Message::Image { url, dimensions, sender } => { /* handle image */ }
        Message::System { content } => { /* handle system message */ }
    }
}

The match is exhaustive—if you add a new variant to Message, the compiler will flag every match statement that doesn’t handle it. An agent can reason about this behavior locally, without holding complex class trees in context, and the compiler ensures nothing is forgotten.

Explicit Over Implicit

Rust favors making behavior visible in the code. There’s no duck typing where a method might exist on an object. There’s no metaprogramming magic that adds methods at runtime. What you see in the code is what happens.

This explicitness is particularly valuable for agents. They don’t need to infer behavior from runtime traces or documentation. If a struct implements a trait, that implementation is declared explicitly. If a function can fail, the return type shows it. Pattern matching with exhaustive match forces handling all cases—the compiler ensures nothing is forgotten.

Cargo.toml: The Dependency Map

All dependencies are declared in one place with pinned versions. No implicit or transitive dependency confusion. An agent can open Cargo.toml and immediately see what external crates are available, what versions are in use, and what features are enabled.

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }

This is the complete picture. No hidden dependencies pulled in by environment variables or global configuration files.

Staying Clean During Iteration

Agents iterate rapidly, trying different approaches. This is a strength—they can explore solution spaces quickly—but it creates a problem. Dead code, unused imports, and abandoned functions accumulate. This cruft pollutes the codebase and wastes context on future iterations. The agent has to read past code that no longer matters.

The Compiler Enforces Cleanup

Rust warns on unused variables, functions, and imports:

warning: unused variable: `result`
 --> src/main.rs:12:9
  |
12|     let result = compute_value();
  |         ^^^^^^ help: if this is intentional, prefix it with an underscore: `_result`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: function `old_implementation` is never used
  --> src/lib.rs:45:4
   |
45 | fn old_implementation() {
   |    ^^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(dead_code)]` on by default

These warnings are hard to ignore. They appear on every compilation. They can be promoted to errors with a single configuration change. This keeps the codebase lean and navigable—dead code doesn’t stick around because the compiler keeps pointing at it.

Clippy: Automated Code Quality

Clippy, Rust’s linter, catches common mistakes and suggests idiomatic patterns. It’s another feedback layer in the agentic loop, and critically, it explains why something is problematic:

warning: this `if let` can be collapsed into the outer `if let`
  --> src/parser.rs:34:9
   |
34 | /         if let Some(value) = outer {
35 | |             if let Some(inner) = value.inner {
36 | |                 process(inner);
37 | |             }
38 | |         }
   | |_________^
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
help: collapse nested if-let patterns
   |
34 |         if let Some(Some(inner)) = outer.map(|v| v.inner) {
35 |             process(inner);
36 |         }
   |

An agent can read this, understand the improvement, and apply it. The explanation prevents the same mistake from recurring.

Consistent Training, Consistent Output

There’s an often-overlooked advantage Rust has for LLM-based tools: consistency in training data.

rustfmt enforces a single canonical style. Nearly all Rust code follows identical formatting. This isn’t just aesthetically pleasant—it means the LLM pretraining data is consistent. Fewer style variations to learn. No averaging across conflicting conventions.

The practical result is that agents generate more idiomatic Rust by default. They’ve seen more consistent examples during training than they have for most other languages, where style varies by project, team, and individual preference.

This consistency extends beyond formatting. Cargo handles build, test, format, lint, documentation, and publishing. Every Rust project uses the same commands: cargo build, cargo test, cargo clippy, cargo fmt. Agents don’t need to learn project-specific build incantations or navigate different build systems. The commands are predictable across the entire ecosystem.

Beyond the Core

Several other Rust characteristics deserve brief mention, though they’re general strengths rather than specific to agentic workflows.

Single binary deployment eliminates runtime dependency reasoning. When an agent generates Rust code, the compiled artifact is self-contained. No questions about which Python version is installed or whether the right Node modules exist.

WebAssembly support means the same code can target browser or edge environments with a compilation flag change. An agent doesn’t need to learn separate web and server paradigms.

FFI capabilities provide interoperability with C for projects that need it, enabling Rust to be introduced incrementally into existing codebases.

Conclusion

Rust’s strict compiler is often cited as a barrier for human developers—a steep learning curve, fights with the borrow checker, lifetimes that seem arcane until they click. These complaints are valid from a certain perspective.

For agents, that strictness becomes an asset. Fast feedback. Clear contracts. No hidden behavior. The language guides agents toward correct code through explicit types, helpful error messages, and enforced cleanup. The very features that slow down a human’s first week with Rust accelerate an agent’s ability to converge on working code.

If you’ve been using agentic coding tools with Python or JavaScript or TypeScript, consider an experiment with Rust. The compile-check-fix loop that feels onerous when you’re learning the language becomes a productivity multiplier when an agent is doing the iteration. The compiler isn’t fighting you—it’s collaborating. And for agents that learn from each error message, that collaboration compounds quickly.