mirror of
https://github.com/leonardomso/rust-skills.git
synced 2026-09-14 19:33:21 +08:00
0016d5cfb2
Includes rules for: - Ownership and borrowing patterns - Error handling with thiserror/anyhow - Memory management and allocation - API design following Rust guidelines - Async/Tokio patterns - Performance optimization - Naming conventions - Type safety - Testing strategies - Documentation standards - Project structure - Linting configuration - Common anti-patterns to avoid
4.0 KiB
4.0 KiB
err-custom-type
Define custom error types for domain-specific failures
Why It Matters
Generic errors like String, Box<dyn Error>, or catch-all enums obscure what can actually go wrong. Custom error types document failure modes in the type system, enable pattern matching for specific handling, and provide clear API contracts. They make your code self-documenting and help callers handle errors appropriately.
Bad
// Generic string errors - no structure
fn validate_user(user: &User) -> Result<(), String> {
if user.name.is_empty() {
return Err("Name is empty".to_string());
}
if user.age > 150 {
return Err("Age is invalid".to_string());
}
Ok(())
}
// Caller can't match on specific errors
match validate_user(&user) {
Ok(()) => save(user),
Err(msg) => {
// Can only do string comparison - fragile!
if msg.contains("Name") {
prompt_for_name()
}
}
}
Good
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ValidationError {
#[error("name cannot be empty")]
EmptyName,
#[error("name exceeds maximum length of {max} characters")]
NameTooLong { max: usize, actual: usize },
#[error("invalid age {0}: must be between 0 and 150")]
InvalidAge(u8),
#[error("email format is invalid: {0}")]
InvalidEmail(String),
}
fn validate_user(user: &User) -> Result<(), ValidationError> {
if user.name.is_empty() {
return Err(ValidationError::EmptyName);
}
if user.name.len() > 100 {
return Err(ValidationError::NameTooLong {
max: 100,
actual: user.name.len()
});
}
if user.age > 150 {
return Err(ValidationError::InvalidAge(user.age));
}
Ok(())
}
// Caller can match specifically
match validate_user(&user) {
Ok(()) => save(user),
Err(ValidationError::EmptyName) => prompt_for_name(),
Err(ValidationError::InvalidAge(age)) => {
show_error(&format!("Please enter a valid age (you entered {})", age))
}
Err(e) => show_error(&e.to_string()),
}
Error Type Design Guidelines
// 1. Group related errors in domain-specific enums
#[derive(Error, Debug)]
pub enum AuthError {
#[error("invalid credentials")]
InvalidCredentials,
#[error("account locked after {attempts} failed attempts")]
AccountLocked { attempts: u32 },
#[error("token expired")]
TokenExpired,
}
#[derive(Error, Debug)]
pub enum PaymentError {
#[error("insufficient funds: need {required}, have {available}")]
InsufficientFunds { required: Decimal, available: Decimal },
#[error("card declined: {reason}")]
CardDeclined { reason: String },
}
// 2. Include relevant data for error handling/display
#[derive(Error, Debug)]
pub enum FileError {
#[error("file not found: {path}")]
NotFound { path: PathBuf },
#[error("permission denied for {path}")]
PermissionDenied { path: PathBuf },
}
// 3. Consider #[non_exhaustive] for public APIs
#[derive(Error, Debug)]
#[non_exhaustive] // Allows adding variants without breaking changes
pub enum ApiError {
#[error("rate limited")]
RateLimited,
#[error("not found")]
NotFound,
}
When to Use What
| Error Pattern | Use Case |
|---|---|
| Custom enum | Library with specific failure modes |
thiserror |
Libraries needing std::error::Error |
anyhow::Error |
Applications, prototypes |
| Struct with source | Single error type with wrapped cause |
Struct-Based Errors
For single error types with rich context:
#[derive(Error, Debug)]
#[error("query failed for table '{table}' with filter '{filter}'")]
pub struct QueryError {
pub table: String,
pub filter: String,
#[source]
pub source: DatabaseError,
}
See Also
- err-thiserror-lib - thiserror for error definitions
- err-anyhow-app - When to use anyhow instead
- api-non-exhaustive - Forward-compatible enums