Tinker AI
Read reviews
intermediate 4 min read

Cursor rules for Rust error handling: anyhow vs thiserror, propagating correctly

Published 2026-01-31 by Owner

Rust has two dominant error handling crates with different use cases: anyhow for application-level errors and thiserror for library-level structured errors. Cursor’s training data has both patterns, and defaults drift between them unpredictably. The result on a project: inconsistent error handling that needs cleanup in review.

A focused .cursorrules section fixes this.

The pattern problem

Without explicit rules, Cursor’s first-attempt suggestions for error handling vary:

  • Sometimes returns Result<T, Box<dyn Error>>
  • Sometimes uses anyhow::Result<T>
  • Sometimes defines a custom error enum with thiserror
  • Sometimes uses ? to propagate without conversion

All of these are valid Rust. None of them is wrong in isolation. But they shouldn’t coexist in one project — a codebase that mixes them is harder to maintain.

The rule

Add to .cursorrules or .cursor/rules/rust.mdc:

# Rust error handling

This project uses thiserror for library/domain errors and anyhow for
application errors. The split:

- src/lib.rs and any code in src/ that's part of the public library:
  thiserror with explicit error enums
- src/bin/, examples/, and the main binary's error path: anyhow

Specific rules:

1. Define error types in src/error.rs at the workspace root and re-export
   from src/lib.rs

2. Domain errors are enums with thiserror::Error:

```rust
#[derive(thiserror::Error, Debug)]
pub enum DomainError {
    #[error("not found: {0}")]
    NotFound(String),
    #[error("invalid input: {reason}")]
    InvalidInput { reason: String },
    #[error(transparent)]
    Db(#[from] sqlx::Error),
}
  1. Library functions return Result<T, DomainError> or a more specific error type. Never return anyhow::Result from library code.

  2. Application code (main.rs, integration glue) can use anyhow::Result for ergonomic error chaining. Use .context() for adding context.

  3. The ? operator handles error propagation. Use From<X> for Y conversions instead of manual map_err where possible.

  4. For external errors that need wrapping, define an enum variant (#[from]) rather than converting at the call site.

  5. Test functions can use anyhow::Result for simplicity.


This is about 40 lines of rule. The cost is in writing it once. The benefit is consistent Rust error handling across the project.

## What changes after the rule

Concrete examples of how Cursor's suggestions improve.

**Before the rule, asking for a function that fetches a user:**

```rust
pub fn fetch_user(id: i64) -> Result<User, Box<dyn std::error::Error>> {
    let db = get_db()?;
    let user = db.query_one("SELECT * FROM users WHERE id = ?", &[&id])?;
    Ok(user.into())
}

Box<dyn Error> is a generic catch-all. It works but loses type information.

After the rule, the same function:

pub fn fetch_user(id: i64) -> Result<User, DomainError> {
    let db = get_db()?;
    let user = db.query_one("SELECT * FROM users WHERE id = ?", &[&id])?;
    Ok(user.into())
}

Now the error type is structured. Callers can match on specific cases. The ? operator works because DomainError has From<sqlx::Error> (assumed defined).

The functional code is the same. The error contract is meaningfully better.

A specific failure to address

A pattern I see in Rust+AI work that the rule prevents:

Cursor sometimes generates code that uses .expect("...") or .unwrap() because the error handling is “obvious” in context. For example:

let config = std::fs::read_to_string("config.toml").unwrap();

In a binary, this might be acceptable for a startup config that you’d want to crash on. In library code, it’s almost never appropriate.

Adding to the rules:

8. Never use unwrap() or expect() in library code. If you can't handle
   the error, return it. unwrap() is OK in tests; expect() with a clear
   message is OK in main.rs for unrecoverable startup failures only.

After this, Cursor’s suggestions stop including unwrap() in library code. The model follows the explicit instruction.

Error context for debugging

A nuance: anyhow’s .context() is great for adding context. Cursor doesn’t always use it.

Adding:

9. When propagating errors with anyhow, add context that helps debugging:
   `result.context("failed to load config from disk")?`
   Avoid vague contexts like "operation failed". The context should
   explain what was being attempted, not what failed.

After this rule, Cursor’s anyhow-using code has more useful error chains. Debugging issues in production becomes meaningfully easier because the errors say what they were trying to do.

What about chained crate-specific errors?

For libraries that integrate with other libraries (sqlx, reqwest, serde, tokio), each has its own error types. The pattern is:

#[derive(thiserror::Error, Debug)]
pub enum DomainError {
    #[error(transparent)]
    Db(#[from] sqlx::Error),
    #[error(transparent)]
    Http(#[from] reqwest::Error),
    #[error(transparent)]
    Serde(#[from] serde_json::Error),
    // ... domain-specific variants ...
}

This lets ? propagate from any of these external errors automatically. Adding a rule:

10. For library functions that interact with external crates (sqlx,
    reqwest, etc.), define From<TheirError> for our error enum via
    #[from] on the relevant variant. This enables ? without explicit
    conversion.

After this, Cursor adds the right #[from] variants when generating new error types.

Worth the investment

Writing the error handling rules takes maybe 30 minutes. The benefit is:

  • Consistent error handling across the codebase
  • Cursor’s suggestions match what you want on first attempt
  • New engineers (human or AI) follow the patterns automatically
  • Code review can focus on logic rather than error-handling style

For Rust projects of any meaningful size, this is one of the higher-leverage rule sets to write.

Adapting to your project

The specific rules above are one workable pattern. Your project may have different conventions:

  • Some projects use only anyhow, even in libraries (acceptable for binary-only crates)
  • Some projects use only thiserror, even in main.rs (more verbose but more typed)
  • Some projects use eyre instead of anyhow (drop-in replacement with different defaults)

Whatever your project’s choice, document it explicitly. The point isn’t which crate; it’s consistency.

A test of the rules

The way I verify the rules are working: ask Cursor to generate a new function with error-handling needs, in a fresh chat. If the output matches my conventions on first attempt, the rules are working. If it doesn’t, the rules need refinement.

Iteration on the rules over the first few weeks of a project produces a stable foundation. After that, the rules stop changing and the consistency benefits compound.