rust-error-handling
Rust error handling with anyhow and thiserror
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
rust-error-handling
This skill covers error handling patterns in Rust using anyhow for application code and thiserror for library/domain-specific errors. Script-kit-gpui uses both crates strategically based on context.
When to Use Each
| Crate | Use Case | Returns |
|---|---|---|
| anyhow | Application code, internal functions, quick prototyping | anyhow::Result<T> |
| thiserror | Public APIs, domain errors, when callers need to match on variants | Custom error enum |
Rule of thumb: If the error needs to cross a module boundary or callers might want to handle specific cases differently, use thiserror. Otherwise, use anyhow.
anyhow
anyhow provides a flexible error type for application-level error handling. It wraps any std::error::Error and adds context.
Core API
use anyhow::{anyhow, bail, Context, Result};
// Result<T> is an alias for Result<T, anyhow::Error>
fn load_config() -> Result<Config> {
let content = std::fs::read_to_string("config.json")?;
let config: Config = serde_json::from_str(&content)?;
Ok(config)
}
Key Macros
// anyhow! - Create an ad-hoc error
return Err(anyhow!("Failed to parse shortcut: {}", shortcut));
// bail! - Early return with error (shorthand for return Err(anyhow!(...)))
if id.is_empty() {
bail!("Entry not found: {}", id);
}
// ensure! - Return error if condition is false
ensure!(count > 0, "Count must be positive, got {}", count);
Adding Context
The Context trait is essential for debugging. Always add context to low-level errors:
use anyhow::{Context, Result};
// .context() - Static context message
let conn = Connection::open(&db_path)
.context("Failed to open AI chats database")?;
// .with_context() - Dynamic context (use when you need formatting)
std::fs::create_dir_all(&dir)
.with_context(|| format!("Failed to create directory: {}", dir.display()))?;
Output when error occurs:
Error: Failed to open AI chats database
Caused by:
No such file or directory (os error 2)
thiserror
thiserror provides derive macros for implementing std::error::Error. Use it when you need structured, matchable errors.
Basic Usage
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ShortcutParseError {
#[error("shortcut string is empty")]
Empty,
#[error("shortcut has no key, only modifiers")]
MissingKey,
#[error("unknown token '{0}' in shortcut")]
UnknownToken(String),
#[error("unknown key '{0}'")]
UnknownKey(String),
}
Attributes
#[derive(Error, Debug)]
pub enum ScriptKitError {
// Basic message
#[error("Configuration error: {0}")]
Config(String),
// Named fields with interpolation
#[error("Script execution failed: {message}")]
ScriptExecution {
message: String,
script_path: Option<String>,
},
// #[from] - Auto-implement From trait for ? operator
#[error("Failed to parse protocol message: {0}")]
ProtocolParse(#[from] serde_json::Error),
// #[source] - Mark the underlying error (for error chain)
#[error("Theme loading failed for '{path}': {source}")]
ThemeLoad {
path: String,
#[source]
source: std::io::Error,
},
}
Attribute Reference
| Attribute | Purpose | Example |
|---|---|---|
#[error("...")] |
Display message with interpolation | #[error("Not found: {0}")] |
#[from] |
Auto-implement From<T> for ? |
Io(#[from] io::Error) |
#[source] |
Mark error source for chaining | #[source] source: io::Error |
#[error(transparent)] |
Delegate Display to inner error | For wrapper types |
Usage in script-kit-gpui
Pattern 1: Domain Errors with thiserror
From src/error.rs - Application-wide error types:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ScriptKitError {
#[error("Script execution failed: {message}")]
ScriptExecution {
message: String,
script_path: Option<String>,
},
#[error("Failed to parse protocol message: {0}")]
ProtocolParse(#[from] serde_json::Error),
#[error("Theme loading failed for '{path}': {source}")]
ThemeLoad {
path: String,
#[source]
source: std::io::Error,
},
}
impl ScriptKitError {
// Add helper methods for error handling
pub fn severity(&self) -> ErrorSeverity {
match self {
Self::ScriptExecution { .. } => ErrorSeverity::Error,
Self::ProtocolParse(_) => ErrorSeverity::Warning,
// ...
}
}
}
Pattern 2: Module-Specific Errors
From src/keyboard_monitor.rs:
#[derive(Error, Debug)]
pub enum KeyboardMonitorError {
#[error("Accessibility permissions not granted. Please enable in System Preferences > Privacy & Security > Accessibility")]
AccessibilityNotGranted,
#[error("Failed to create event tap - this may indicate accessibility permissions issue")]
EventTapCreationFailed,
#[error("Monitor is already running")]
AlreadyRunning,
}
Pattern 3: anyhow with Context for Internal Functions
From src/ai/storage.rs:
use anyhow::{Context, Result};
pub fn create_chat(id: &str, model: &str) -> Result<()> {
let guard = DB.read()
.map_err(|e| anyhow::anyhow!("DB lock error: {}", e))?;
let conn = guard.as_ref()
.ok_or_else(|| anyhow::anyhow!("AI database not initialized"))?;
conn.execute(
"INSERT INTO chats ...",
params![id, model],
).context("Failed to create chat")?;
Ok(())
}
Pattern 4: Error Logging Extensions
From src/error.rs - Extension traits for ergonomic error handling:
pub trait ResultExt<T> {
fn log_err(self) -> Option<T>;
fn warn_on_err(self) -> Option<T>;
}
impl<T, E: std::fmt::Debug> ResultExt<T> for Result<T, E> {
#[track_caller]
fn log_err(self) -> Option<T> {
match self {
Ok(value) => Some(value),
Err(error) => {
let caller = std::panic::Location::caller();
error!(
error = ?error,
file = caller.file(),
line = caller.line(),
"Operation failed"
);
None
}
}
}
}
// Usage:
some_fallible_op().log_err(); // Logs error, returns Option<T>
Error Context
Static Context (prefer when message is fixed)
conn.execute(sql, params)
.context("Failed to create chat")?;
Dynamic Context (when you need variables)
std::fs::write(&path, content)
.with_context(|| format!("Failed to write script file: {}", path.display()))?;
Context Stacking
Context messages stack to create a trace:
fn load_user_settings() -> Result<Settings> {
let path = get_settings_path()?;
let content = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read {}", path.display()))?;
let settings: Settings = serde_json::from_str(&content)
.context("Failed to parse settings JSON")?;
Ok(settings)
}
Output on error:
Error: Failed to parse settings JSON
Caused by:
0: expected value at line 1 column 1
Converting Between Types
From thiserror to anyhow (automatic with ?)
fn process() -> anyhow::Result<()> {
let shortcut = Shortcut::parse(input)?; // ShortcutParseError -> anyhow::Error
Ok(())
}
From anyhow to thiserror (use #[error(transparent)])
#[derive(Error, Debug)]
pub enum MyError {
#[error(transparent)]
Other(#[from] anyhow::Error),
}
Manual From implementations
impl From<std::io::Error> for ScriptKitError {
fn from(err: std::io::Error) -> Self {
ScriptKitError::FileWatch(err.to_string())
}
}
Best Practices
1. Choose the Right Tool
// Internal function - use anyhow
fn load_internal_config() -> anyhow::Result<Config> { ... }
// Public API - use thiserror
pub fn parse_shortcut(s: &str) -> Result<Shortcut, ShortcutParseError> { ... }
2. Always Add Context to I/O Operations
// BAD - unhelpful error message
let content = std::fs::read_to_string(path)?;
// GOOD - tells you what failed
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read config from {}", path.display()))?;
3. Use bail! for Early Returns
// Instead of:
if !is_valid {
return Err(anyhow!("Invalid input"));
}
// Use:
if !is_valid {
bail!("Invalid input");
}
4. Make thiserror Variants Actionable
// BAD - what should the user do?
#[error("Permission error")]
PermissionError,
// GOOD - tells user how to fix it
#[error("Accessibility permissions not granted. Please enable in System Preferences > Privacy & Security > Accessibility")]
AccessibilityNotGranted,
5. Use map_err for Lock Poisoning
let guard = MANAGER.lock()
.map_err(|e| anyhow::anyhow!("Lock poisoned: {}", e))?;
6. Add Severity/Category Methods to Domain Errors
impl ScriptKitError {
pub fn severity(&self) -> ErrorSeverity { ... }
pub fn user_message(&self) -> String { ... }
pub fn is_recoverable(&self) -> bool { ... }
}
Anti-patterns
1. Unwrapping in Library Code
// BAD
let value = some_result.unwrap();
// GOOD
let value = some_result.context("Failed to get value")?;
2. Losing Error Context
// BAD - original error is lost
.map_err(|_| anyhow!("Something failed"))
// GOOD - preserves error chain
.context("Something failed")
3. Overly Generic Error Messages
// BAD
.context("Failed")?;
// GOOD
.context("Failed to parse user configuration file")?;
4. Using String Errors
// BAD
fn risky() -> Result<(), String> {
Err("It broke".to_string())
}
// GOOD
fn risky() -> anyhow::Result<()> {
bail!("Operation failed: specific reason");
}
5. Matching on anyhow::Error
// BAD - anyhow is opaque
match err {
// Can't match variants
}
// GOOD - use thiserror when you need to match
match err {
ShortcutParseError::Empty => ...,
ShortcutParseError::UnknownKey(k) => ...,
}
6. Ignoring Errors Silently
// BAD
let _ = some_fallible_op();
// BETTER - log it
some_fallible_op().log_err();
// BEST - propagate if caller should handle
some_fallible_op()?;
Debug Macro
For "impossible" states, use the debug_panic! macro from src/error.rs:
// Panics in debug, logs in release
debug_panic!("Invariant violated: counter was {}", counter);
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-02-03
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!