Marc Love

The Rust Language + Toolchain: Robust Guardrails Against Agent Hallucinations, Dead Code, and Runtime Bugs

Rust Quality Control

Agentic coding tools are rapidly changing how we write software. Claude Code, Codex, Cursor, etc have become the primary means of generating code for a growing number of developers. But anyone who has spent 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 files. They leave behind dead code and cruft during iteration. They struggle to reason about deep inheritance hierarchies and implicit behaviors. And the errors surface late—at runtime or in production—after wasted cycles and frustrating debugging sessions.

These aren’t bugs in the tools themselves. They’re fundamental limitations of language models: bounded context, probabilistic generation, and challenges with ground-truth verification. The agentic loop helps by catching errors through execution and iteration–similar to how we as human engineers debug and troubleshoot–but it doesn’t eliminate the underlying structural weaknesses of LLMs. The question isn’t whether agents make mistakes, but whether the feedback loop catches them before you or a user does.

This is where Rust becomes interesting. Other languages have tooling that provides feedback, but Rust’s toolchain is exceptional—both in scope and in how much it catches at compile time. The type system, borrow checker, and integrated tools produce precise error messages that provide exceptional feedback for the agentic loop. If you’re working with agentic coding tools, Rust’s design makes it unusually well-suited choice of language for agentic-enabled development.

Why The Feedback Loop Matters for Agents

Agentic coding tools operate in loops: generate code, check results, revise. This feedback loop is the core mechanism for overcoming many of the limitations of one-shot code generation. LLMs are, in effect, able to check their own work and iterate. The quality of output and efficiency of arriving at a correct solution depends heavily on how tight, rich, 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, the compiler immediately warns or throws an error. And the feedback it provides 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 in many 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

The Rust compiler follows a design philosophy of writing errors that, if possible, contain specific and actionable resolution hints.

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, 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 many other languages with the convention of having separate 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 a notification system in an object-oriented language versus Rust. In Python, you might build an inheritance hierarchy:

class Notification:
    def __init__(self, recipient):
        self.recipient = recipient
        self.timestamp = datetime.now()

    def send(self):
        raise NotImplementedError

class EmailNotification(Notification):
    def __init__(self, recipient, subject):
        super().__init__(recipient)
        self.subject = subject

    def send(self):
        # Send email logic
        pass

class UrgentEmailNotification(EmailNotification):
    def __init__(self, recipient, subject, priority):
        super().__init__(recipient, subject)
        self.priority = priority

    def send(self):
        self.mark_as_urgent()
        super().send()

    def mark_as_urgent(self):
        # Urgent marking logic
        pass

When an agent works with this code, it must trace through three levels of inheritance to understand what UrgentEmailNotification.send() does. If Notification lives in base.py, EmailNotification in email.py, and UrgentEmailNotification in urgent.py, the agent needs to read all three files and hold the inheritance chain in context. If a method is overridden somewhere in the middle, that behavior is invisible until you trace through each level.

In Rust, the same functionality uses explicit composition:

struct Notification {
    recipient: UserId,
    timestamp: DateTime,
}

trait Sendable {
    fn send(&self) -> Result<(), SendError>;
}

struct EmailContent {
    subject: String,
    body: String,
}

struct Email {
    notification: Notification,
    content: EmailContent,
}

impl Sendable for Email {
    fn send(&self) -> Result<(), SendError> {
        // Send email logic
        Ok(())
    }
}

struct UrgentEmail {
    email: Email,
    priority: Priority,
}

impl UrgentEmail {
    fn mark_as_urgent(&self) {
        // Urgent marking logic
    }
}

impl Sendable for UrgentEmail {
    fn send(&self) -> Result<(), SendError> {
        self.mark_as_urgent();
        self.email.send()
    }
}

Everything is explicit. UrgentEmail contains an Email, which contains a Notification. There’s no implicit method resolution through inheritance chains. An agent reading UrgentEmail::send() sees exactly what it does: mark urgent, delegate to email.send(). No hidden behavior inherited from parent classes. No super calls that require tracing back through the hierarchy. The structure is visible in the type definitions, and behavior is implemented directly on each type.

Exhaustive Pattern Matching

Rust’s match expressions require handling all possible cases; the compiler enforces exhaustiveness. When an agent generates a match statement, it must account for every variant, or the code won’t compile.

Agents often forget edge cases. They focus on the primary paths and overlook the variants that occur less frequently. In languages with non-exhaustive matching, these forgotten cases become runtime failures. In Rust, they’re compile-time errors that the agent must fix before moving forward.

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 */ }
    }
}

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.

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 cruft: dead code, unused imports, and abandoned functions accumulate. This 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

Rust has an advantage over many other languages for LLM-generated code: consistency in training data.

rustfmt enforces a single canonical style. Nearly all Rust code follows identical formatting, which 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.

Rust & Coding Agents: An Ideal Match

Rust’s strict compiler is often cited as a barrier for human developers—a steep learning curve, fights with the borrow checker, lifetimes that are initially confusing and hard to grasp.

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.

You might notice that the properties that make Rust effective for agents are the same properties that make it an exceptional language for human engineers. Obviously, Rust wasn’t designed for agents—it was designed for humans who need to write reliable systems software. The tight feedback loop, the explicit contracts, the exhaustive error handling, the composition over inheritance—these are all features that help human developers catch bugs early, reason about code locally, and confidently deploy “correct” code.

Agents simply take advantage of these features and guarantees. Where a human might appreciate a helpful compiler error once, an agent can better understand that error and quickly apply a fix on the first try. Where a human benefits from explicit types when returning to code after six months, an agent benefits from these types as a form of inline prompting. The guardrails that guide human developers toward correct code do the same for agents.

This means that investing in Rust proficiency pays dividends in both human and agentic workflows. The language you learn to write better systems code is the same language that enables more effective agentic development. It’s also worth mentioning that Rust’s steep learning curve is flattened significantly when you have coding LLMs at your disposal—they can explain the aspects of the language that you may find hard to grasp in easy to understand terms.

If you’ve been using agentic coding tools with Python or JavaScript or TypeScript, consider experimenting with a Rust project. 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.