Add: windows mvp - transparent bugs not fixed
This commit is contained in:
404
skills/m06-error-handling/patterns/error-patterns.md
Normal file
404
skills/m06-error-handling/patterns/error-patterns.md
Normal file
@@ -0,0 +1,404 @@
|
||||
# Error Handling Patterns
|
||||
|
||||
## The ? Operator
|
||||
|
||||
### Basic Usage
|
||||
```rust
|
||||
fn read_config() -> Result<Config, io::Error> {
|
||||
let content = std::fs::read_to_string("config.toml")?;
|
||||
let config: Config = toml::from_str(&content)?; // needs From impl
|
||||
Ok(config)
|
||||
}
|
||||
```
|
||||
|
||||
### With Different Error Types
|
||||
```rust
|
||||
use std::error::Error;
|
||||
|
||||
// Box<dyn Error> for quick prototyping
|
||||
fn process() -> Result<(), Box<dyn Error>> {
|
||||
let file = std::fs::read_to_string("data.txt")?;
|
||||
let num: i32 = file.trim().parse()?; // different error type
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Conversion with From
|
||||
```rust
|
||||
#[derive(Debug)]
|
||||
enum MyError {
|
||||
Io(std::io::Error),
|
||||
Parse(std::num::ParseIntError),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for MyError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
MyError::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::num::ParseIntError> for MyError {
|
||||
fn from(err: std::num::ParseIntError) -> Self {
|
||||
MyError::Parse(err)
|
||||
}
|
||||
}
|
||||
|
||||
fn process() -> Result<i32, MyError> {
|
||||
let content = std::fs::read_to_string("num.txt")?; // auto-converts
|
||||
let num: i32 = content.trim().parse()?; // auto-converts
|
||||
Ok(num)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Type Design
|
||||
|
||||
### Simple Enum Error
|
||||
```rust
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ConfigError {
|
||||
NotFound,
|
||||
InvalidFormat,
|
||||
MissingField(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConfigError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ConfigError::NotFound => write!(f, "configuration file not found"),
|
||||
ConfigError::InvalidFormat => write!(f, "invalid configuration format"),
|
||||
ConfigError::MissingField(field) => write!(f, "missing field: {}", field),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ConfigError {}
|
||||
```
|
||||
|
||||
### Error with Source (Wrapping)
|
||||
```rust
|
||||
#[derive(Debug)]
|
||||
pub struct AppError {
|
||||
kind: AppErrorKind,
|
||||
source: Option<Box<dyn std::error::Error + Send + Sync>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum AppErrorKind {
|
||||
Config,
|
||||
Database,
|
||||
Network,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AppError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.kind {
|
||||
AppErrorKind::Config => write!(f, "configuration error"),
|
||||
AppErrorKind::Database => write!(f, "database error"),
|
||||
AppErrorKind::Network => write!(f, "network error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AppError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
self.source.as_ref().map(|e| e.as_ref() as _)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using thiserror
|
||||
|
||||
### Basic Usage
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DataError {
|
||||
#[error("file not found: {path}")]
|
||||
NotFound { path: String },
|
||||
|
||||
#[error("invalid data format")]
|
||||
InvalidFormat,
|
||||
|
||||
#[error("IO error")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("parse error: {0}")]
|
||||
Parse(#[from] std::num::ParseIntError),
|
||||
}
|
||||
|
||||
// Usage
|
||||
fn load_data(path: &str) -> Result<Data, DataError> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.map_err(|_| DataError::NotFound { path: path.to_string() })?;
|
||||
let num: i32 = content.trim().parse()?; // auto-converts with #[from]
|
||||
Ok(Data { value: num })
|
||||
}
|
||||
```
|
||||
|
||||
### Transparent Wrapper
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error(transparent)]
|
||||
pub struct MyError(#[from] InnerError);
|
||||
|
||||
// Useful for newtype error wrappers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using anyhow
|
||||
|
||||
### For Applications
|
||||
```rust
|
||||
use anyhow::{Context, Result, bail, ensure};
|
||||
|
||||
fn process_file(path: &str) -> Result<Data> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.context("failed to read config file")?;
|
||||
|
||||
ensure!(!content.is_empty(), "config file is empty");
|
||||
|
||||
let data: Data = serde_json::from_str(&content)
|
||||
.context("failed to parse JSON")?;
|
||||
|
||||
if data.version < 1 {
|
||||
bail!("unsupported config version: {}", data.version);
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let data = process_file("config.json")
|
||||
.context("failed to load configuration")?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Error Chain
|
||||
```rust
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
fn deep_function() -> Result<()> {
|
||||
std::fs::read_to_string("missing.txt")
|
||||
.context("failed to read file")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn middle_function() -> Result<()> {
|
||||
deep_function()
|
||||
.context("failed in deep function")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn top_function() -> Result<()> {
|
||||
middle_function()
|
||||
.context("failed in middle function")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Error output shows full chain:
|
||||
// Error: failed in middle function
|
||||
// Caused by:
|
||||
// 0: failed in deep function
|
||||
// 1: failed to read file
|
||||
// 2: No such file or directory (os error 2)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Option Handling
|
||||
|
||||
### Converting Option to Result
|
||||
```rust
|
||||
fn find_user(id: u32) -> Option<User> { ... }
|
||||
|
||||
// Using ok_or for static error
|
||||
fn get_user(id: u32) -> Result<User, &'static str> {
|
||||
find_user(id).ok_or("user not found")
|
||||
}
|
||||
|
||||
// Using ok_or_else for dynamic error
|
||||
fn get_user(id: u32) -> Result<User, String> {
|
||||
find_user(id).ok_or_else(|| format!("user {} not found", id))
|
||||
}
|
||||
```
|
||||
|
||||
### Chaining Options
|
||||
```rust
|
||||
fn get_nested_value(data: &Data) -> Option<&str> {
|
||||
data.config
|
||||
.as_ref()?
|
||||
.nested
|
||||
.as_ref()?
|
||||
.value
|
||||
.as_deref()
|
||||
}
|
||||
|
||||
// Equivalent with and_then
|
||||
fn get_nested_value(data: &Data) -> Option<&str> {
|
||||
data.config
|
||||
.as_ref()
|
||||
.and_then(|c| c.nested.as_ref())
|
||||
.and_then(|n| n.value.as_deref())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Result Combinators
|
||||
|
||||
### map and map_err
|
||||
```rust
|
||||
fn parse_port(s: &str) -> Result<u16, ParseError> {
|
||||
s.parse::<u16>()
|
||||
.map_err(|e| ParseError::InvalidPort(e))
|
||||
}
|
||||
|
||||
fn get_url(config: &Config) -> Result<String, Error> {
|
||||
config.url()
|
||||
.map(|u| format!("https://{}", u))
|
||||
}
|
||||
```
|
||||
|
||||
### and_then (flatMap)
|
||||
```rust
|
||||
fn validate_and_save(input: &str) -> Result<(), Error> {
|
||||
validate(input)
|
||||
.and_then(|valid| save(valid))
|
||||
.and_then(|saved| notify(saved))
|
||||
}
|
||||
```
|
||||
|
||||
### unwrap_or and unwrap_or_else
|
||||
```rust
|
||||
// Default value
|
||||
let port = config.port().unwrap_or(8080);
|
||||
|
||||
// Computed default
|
||||
let port = config.port().unwrap_or_else(|| find_free_port());
|
||||
|
||||
// Default for Result
|
||||
let data = load_data().unwrap_or_default();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Early Return vs Combinators
|
||||
|
||||
### Early Return Style
|
||||
```rust
|
||||
fn process(input: &str) -> Result<Output, Error> {
|
||||
let step1 = validate(input)?;
|
||||
if !step1.is_valid {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
|
||||
let step2 = transform(step1)?;
|
||||
let step3 = save(step2)?;
|
||||
|
||||
Ok(step3)
|
||||
}
|
||||
```
|
||||
|
||||
### Combinator Style
|
||||
```rust
|
||||
fn process(input: &str) -> Result<Output, Error> {
|
||||
validate(input)
|
||||
.and_then(|s| {
|
||||
if s.is_valid {
|
||||
Ok(s)
|
||||
} else {
|
||||
Err(Error::Invalid)
|
||||
}
|
||||
})
|
||||
.and_then(transform)
|
||||
.and_then(save)
|
||||
}
|
||||
```
|
||||
|
||||
### When to Use Which
|
||||
|
||||
| Style | Best For |
|
||||
|-------|----------|
|
||||
| Early return (`?`) | Most cases, clearer flow |
|
||||
| Combinators | Functional pipelines, one-liners |
|
||||
| Match | Complex branching on errors |
|
||||
|
||||
---
|
||||
|
||||
## Panic vs Result
|
||||
|
||||
### When to Panic
|
||||
```rust
|
||||
// 1. Unrecoverable programmer error
|
||||
fn get_config() -> &'static Config {
|
||||
CONFIG.get().expect("config must be initialized")
|
||||
}
|
||||
|
||||
// 2. In tests
|
||||
#[test]
|
||||
fn test_parsing() {
|
||||
let result = parse("valid").unwrap(); // OK in tests
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
// 3. Prototype/examples
|
||||
fn main() {
|
||||
let data = load().unwrap(); // OK for quick examples
|
||||
}
|
||||
```
|
||||
|
||||
### When to Return Result
|
||||
```rust
|
||||
// 1. Any I/O operation
|
||||
fn read_file(path: &str) -> Result<String, io::Error>
|
||||
|
||||
// 2. User input validation
|
||||
fn parse_port(s: &str) -> Result<u16, ParseError>
|
||||
|
||||
// 3. Network operations
|
||||
async fn fetch(url: &str) -> Result<Response, Error>
|
||||
|
||||
// 4. Anything that can fail at runtime
|
||||
fn connect(addr: &str) -> Result<Connection, Error>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Context Best Practices
|
||||
|
||||
### Add Context at Boundaries
|
||||
```rust
|
||||
fn load_user_config(user_id: u64) -> Result<Config, Error> {
|
||||
let path = format!("/home/{}/config.toml", user_id);
|
||||
|
||||
std::fs::read_to_string(&path)
|
||||
.context(format!("failed to read config for user {}", user_id))?
|
||||
// NOT: .context("failed to read file") // too generic
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Include Relevant Data
|
||||
```rust
|
||||
// Good: includes the problematic value
|
||||
fn parse_age(s: &str) -> Result<u8, Error> {
|
||||
s.parse()
|
||||
.context(format!("invalid age value: '{}'", s))
|
||||
}
|
||||
|
||||
// Bad: no context about what failed
|
||||
fn parse_age(s: &str) -> Result<u8, Error> {
|
||||
s.parse()
|
||||
.context("parse error")
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user