Result vs Option vs panic
| Type |
When to use |
Example |
Result<T, E> |
Operations expected to fail |
File reading, network requests |
Option<T> |
absence is normal |
Lookups, values that may be empty |
panic! |
bug or invariant violation |
Program logic errors, unrecoverable errors |
unreachable!() |
Code that theoretically will never be reached |
Match exhaustiveness |
Error handling decision
失败是预期的吗?
│
├─ 是 → 这是库代码?
│ ├─ 是 → thiserror(类型化错误)
│ └─ 否 → anyhow(易用性)
│
├─ 否,absence 正常?
│ └─ Option<T>
│
└─ 否,bug 或不变式违规
└─ panic!, assert!
thiserror (library code)
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MyError {
#[error("validation failed: {0}")]
Validation(String),
#[error("IO error: {source}")]
Io {
#[from]
source: std::io::Error,
},
#[error("not found: {entity}:{id}")]
NotFound {
entity: String,
id: u64,
},
}
// 使用 ? 传播
fn read_config() -> Result<Config, MyError> {
let content = std::fs::read_to_string("config.toml")?;
Ok(toml::from_str(&content)?)
}
anyhow (application code)
use anyhow::{Context, Result, bail};
fn process_user(id: u64) -> Result<User> {
let user = db.find_user(id)
.with_context(|| format!("failed to find user {}", id))?;
if !user.is_active {
bail!("user {} is not active", id);
}
Ok(user)
}
// 组合多个错误源
fn complex_operation() -> Result<()> {
let a = operation_a().context("operation A failed")?;
let b = operation_b().context("operation B failed")?;
Ok(())
}
Error design principles
| Scenario |
Recommendation |
| Library code |
thiserror, precise error types |
| Application code |
anyhow, easy to propagate and add context |
| Library depending on library |
Propagate third-party errors (#[from]) |
| Need error codes |
Enum variants |
| Need error chains |
context() + with_context() |
Common anti-patterns
| Anti-pattern |
Problem |
Solution |
Everywhere unwrap() |
panic in libraries |
Use ? |
Box<dyn Error> |
Loss of type information |
thiserror variants |
| Loss of context |
Difficult to debug |
.context() |
| Too many error variants |
Over-engineering |
Simplify or merge |
panic use cases
// 1. 不变量验证(公开 API)
pub fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
panic!("division by zero"); // 公开 API,确保调用者不传入 0
}
a / b
}
// 2. 不可恢复错误
fn start_engine() {
let config = load_critical_config();
if config.is_corrupted() {
panic!("cannot start without valid config");
}
}
// 3. 匹配穷举(理论上的永远执行不到)
fn process_status(status: Status) {
match status {
Status::Running => { /* ... */ }
Status::Stopped => { /* ... */ }
// 未来可能添加新状态
// _ => unreachable!("unknown status: {:?}", status),
}
}
// 4. 内部不变量
assert!(!queue.is_empty(), "queue should never be empty here");
Error chains
// 使用 map_err 转换错误
fn high_level() -> Result<()> {
low_level()
.map_err(|e| MyError::from_low_level(e, "high level operation failed"))
}
// 使用 with_context 添加调用链信息
fn middle_layer() -> Result<()> {
low_level()
.with_context(|| format!("while processing request {}", request_id))?;
Ok(())
}
Best practices
- Library code: precise error types (thiserror)
- Application code: prioritize ease of use (anyhow)
- Propagate errors: use
? instead of unwrap()
- Add context: use
.context() or with_context()
- Preserve error source: use
#[from] to preserve the underlying error
- Distinguish panic scenarios: use panic for bugs, use Result for expected failures