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
3.4 KiB
3.4 KiB
err-result-over-panic
Return
Result<T, E>instead of panicking for recoverable errors
Why It Matters
Panics unwind the stack and crash the thread (or program). They're unrecoverable from the caller's perspective. Result<T, E> gives callers the ability to decide how to handle errors—retry, fallback, propagate, or log. Libraries should almost never panic; applications should minimize panics to truly unrecoverable situations.
Bad
fn parse_config(path: &str) -> Config {
let content = std::fs::read_to_string(path)
.expect("Failed to read config"); // Crashes on missing file
serde_json::from_str(&content)
.expect("Invalid config format") // Crashes on bad JSON
}
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Division by zero!"); // Crashes the program
}
a / b
}
Caller has no chance to recover or provide a fallback.
Good
use thiserror::Error;
#[derive(Error, Debug)]
enum ConfigError {
#[error("Failed to read config file: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid config format: {0}")]
Parse(#[from] serde_json::Error),
}
fn parse_config(path: &str) -> Result<Config, ConfigError> {
let content = std::fs::read_to_string(path)?;
let config = serde_json::from_str(&content)?;
Ok(config)
}
fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
if b == 0 {
return Err("Division by zero");
}
Ok(a / b)
}
// Caller decides how to handle
match parse_config("app.json") {
Ok(config) => run_app(config),
Err(e) => {
eprintln!("Using default config: {}", e);
run_app(Config::default())
}
}
When Panic IS Appropriate
// 1. Bug in the program (invariant violation)
fn get_cached_value(&self, key: &str) -> &Value {
self.cache.get(key).expect("BUG: key was verified to exist")
}
// 2. Setup/initialization that can't reasonably fail
fn main() {
let config = Config::load().expect("Failed to load required config");
// Can't run without config, panic is reasonable
}
// 3. Tests
#[test]
fn test_parse() {
let result = parse("valid input").unwrap(); // unwrap OK in tests
assert_eq!(result, expected);
}
// 4. Examples and prototypes
fn main() {
// Quick prototype, panic is fine
let data = fetch_data().unwrap();
}
Panic vs Result Decision Guide
| Situation | Use |
|---|---|
| File not found | Result |
| Network error | Result |
| Invalid user input | Result |
| Parse error | Result |
| Index out of bounds (from user data) | Result |
| Index out of bounds (internal bug) | Panic |
| Violated internal invariant | Panic |
| Unimplemented code path | Panic (unimplemented!()) |
| Impossible state reached | Panic (unreachable!()) |
Library vs Application
// Library: NEVER panic on user input
pub fn parse(input: &str) -> Result<Ast, ParseError> {
// Always return Result
}
// Application: Can panic at top level for critical failures
fn main() {
if let Err(e) = run() {
eprintln!("Fatal error: {}", e);
std::process::exit(1);
}
}
See Also
- err-thiserror-lib - Define error types for libraries
- err-anyhow-app - Ergonomic errors for applications
- err-no-unwrap-prod - Avoid unwrap in production code
- anti-unwrap-abuse - When unwrap is acceptable