Giao diện
Error Handling Architecture Deep Tech
Error Handling đúng cách: Từ Monads đến Panic Mechanics
Philosophy: Errors are Values
Rust không có exceptions. Errors là values được xử lý explicitly:
rust
// ❌ Exception-based (Java, Python)
// try { riskyOperation(); } catch (Exception e) { ... }
// ✅ Rust: Errors are explicit values
fn risky_operation() -> Result<Success, Error> { ... }
match risky_operation() {
Ok(value) => use(value),
Err(error) => handle(error),
}Result<T, E> Monad
Định nghĩa
rust
// Standard library definition
enum Result<T, E> {
Ok(T), // Success với value T
Err(E), // Failure với error E
}Basic Usage
rust
use std::fs::File;
use std::io::{self, Read};
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // ? operator
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
fn main() {
match read_file("config.txt") {
Ok(contents) => println!("Contents: {}", contents),
Err(e) => eprintln!("Error: {}", e),
}
}The ? Operator
? là syntactic sugar cho early return on error:
rust
// Với ?
fn read_username() -> Result<String, io::Error> {
let mut file = File::open("username.txt")?;
let mut username = String::new();
file.read_to_string(&mut username)?;
Ok(username)
}
// Desugars to:
fn read_username_desugared() -> Result<String, io::Error> {
let mut file = match File::open("username.txt") {
Ok(f) => f,
Err(e) => return Err(e.into()), // Early return
};
let mut username = String::new();
match file.read_to_string(&mut username) {
Ok(_) => {}
Err(e) => return Err(e.into()),
}
Ok(username)
}Monadic Operations
rust
fn process() -> Result<i32, String> {
// map: Transform Ok value
let result: Result<i32, String> = Ok("42".to_string());
let parsed: Result<i32, String> = result.map(|s| s.parse().unwrap());
// map_err: Transform Err value
let result: Result<i32, i32> = Err(404);
let with_msg: Result<i32, String> = result.map_err(|code| format!("Error {}", code));
// and_then: Chain operations that might fail
let result = Ok(10)
.and_then(|x| if x > 5 { Ok(x * 2) } else { Err("too small") })
.and_then(|x| Ok(x + 1));
// Ok(21)
// unwrap_or: Default value on error
let value = Err("failed").unwrap_or(0); // 0
// unwrap_or_else: Lazy default
let value = Err("failed").unwrap_or_else(|e| {
eprintln!("Error: {}", e);
0
});
Ok(42)
}Combining Results
rust
// Collect Vec<Result<T, E>> into Result<Vec<T>, E>
fn parse_all(inputs: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
inputs.iter()
.map(|s| s.parse::<i32>())
.collect() // Stops at first error
}
// Ignore errors, keep only Ok values
fn parse_valid(inputs: &[&str]) -> Vec<i32> {
inputs.iter()
.filter_map(|s| s.parse::<i32>().ok())
.collect()
}
// Partition into successes and failures
fn partition_results(inputs: &[&str]) -> (Vec<i32>, Vec<std::num::ParseIntError>) {
let (oks, errs): (Vec<_>, Vec<_>) = inputs.iter()
.map(|s| s.parse::<i32>())
.partition(Result::is_ok);
(oks.into_iter().map(Result::unwrap).collect(),
errs.into_iter().map(Result::unwrap_err).collect())
}Option<T> Monad
Null Safety Pattern
rust
// Standard library definition
enum Option<T> {
Some(T), // Value exists
None, // No value (null equivalent)
}Usage Patterns
rust
fn find_user(id: u32) -> Option<User> {
// Database lookup...
if id == 1 {
Some(User { name: "Alice".into() })
} else {
None
}
}
fn main() {
// Pattern matching
match find_user(1) {
Some(user) => println!("Found: {}", user.name),
None => println!("Not found"),
}
// if let
if let Some(user) = find_user(1) {
println!("Found: {}", user.name);
}
// Chaining
let name = find_user(1)
.map(|u| u.name)
.unwrap_or_else(|| "Anonymous".into());
}Option Methods
rust
fn option_methods() {
let some: Option<i32> = Some(42);
let none: Option<i32> = None;
// Unwrap variants
some.unwrap(); // 42 (panics if None)
some.expect("must exist"); // 42 (panics with custom message)
some.unwrap_or(0); // 42
none.unwrap_or(0); // 0
none.unwrap_or_default(); // 0 (T: Default)
// Transform
some.map(|x| x * 2); // Some(84)
none.map(|x| x * 2); // None
// Filter
some.filter(|x| *x > 50); // None
some.filter(|x| *x < 50); // Some(42)
// Chain
some.and_then(|x| if x > 0 { Some(x) } else { None });
// Convert to Result
some.ok_or("no value"); // Ok(42)
none.ok_or("no value"); // Err("no value")
}Option vs Result
rust
// Option: "có hoặc không"
fn find_index(items: &[i32], target: i32) -> Option<usize> {
items.iter().position(|&x| x == target)
}
// Result: "thành công hoặc lỗi với lý do"
fn parse_config(path: &str) -> Result<Config, ConfigError> {
let contents = std::fs::read_to_string(path)
.map_err(ConfigError::IoError)?;
serde_json::from_str(&contents)
.map_err(ConfigError::ParseError)
}Custom Error Types
Manual Implementation
rust
use std::fmt;
use std::error::Error;
#[derive(Debug)]
enum AppError {
IoError(std::io::Error),
ParseError(String),
ValidationError { field: String, message: String },
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::IoError(e) => write!(f, "IO error: {}", e),
AppError::ParseError(s) => write!(f, "Parse error: {}", s),
AppError::ValidationError { field, message } => {
write!(f, "Validation error on '{}': {}", field, message)
}
}
}
}
impl Error for AppError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
AppError::IoError(e) => Some(e),
_ => None,
}
}
}
// From implementations for ? operator
impl From<std::io::Error> for AppError {
fn from(err: std::io::Error) -> Self {
AppError::IoError(err)
}
}Using thiserror Crate
rust
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Parse error: {0}")]
ParseError(String),
#[error("Validation error on '{field}': {message}")]
ValidationError {
field: String,
message: String,
},
#[error("Database error: {0}")]
DatabaseError(#[from] sqlx::Error),
#[error("Not found: {0}")]
NotFound(String),
}
// thiserror tự động generate:
// - Display implementation
// - Error trait implementation
// - From implementations (#[from])Error Wrapping Pattern
rust
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ServiceError {
#[error("Failed to fetch user: {0}")]
UserFetch(#[source] DatabaseError),
#[error("Failed to send notification: {0}")]
Notification(#[source] NotificationError),
}
// Usage
fn get_user(id: u32) -> Result<User, ServiceError> {
db::fetch_user(id)
.map_err(ServiceError::UserFetch)
}Panic Mechanics
Khi nào Panic?
rust
// Panic cho unrecoverable errors
fn main() {
panic!("Something went terribly wrong!"); // Explicit panic
let v = vec![1, 2, 3];
v[100]; // Implicit panic: index out of bounds
None::<i32>.unwrap(); // Implicit panic: unwrap on None
}Stack Unwinding vs Abort
Rust có hai panic strategies:
toml
# Cargo.toml
[profile.release]
panic = "unwind" # Default: stack unwinding
# panic = "abort" # Alternative: immediate abortUnwinding (Default):
rust
// 1. Panic triggered
// 2. Unwind stack frame by frame
// 3. Call Drop for each value (cleanup)
// 4. Eventually terminate or catch_unwind
use std::panic;
fn main() {
let result = panic::catch_unwind(|| {
panic!("oops");
});
match result {
Ok(_) => println!("No panic"),
Err(_) => println!("Caught panic!"),
}
}Abort:
rust
// 1. Panic triggered
// 2. Program immediately terminates
// 3. NO cleanup, NO Drop calls
// 4. Smaller binary size (no unwinding code)Unwinding Implementation
┌─────────────────────────────────────────────────────────────┐
│ PANIC UNWINDING │
├─────────────────────────────────────────────────────────────┤
│ main() │
│ │ │
│ ▼ │
│ foo() ─────────────────────────────────────────────────┐ │
│ │ │ │
│ ▼ │ │
│ bar() ─────────────────────────────────────────────┐ │ │
│ │ │ │ │
│ ▼ │ │ │
│ PANIC! │ │ │
│ │ │ │ │
│ ▼ │ │ │
│ Unwind: Drop bar's locals ◄────────────────────────┘ │ │
│ │ │ │
│ ▼ │ │
│ Unwind: Drop foo's locals ◄────────────────────────────┘ │
│ │ │
│ ▼ │
│ catch_unwind hoặc terminate │
└─────────────────────────────────────────────────────────────┘Panic Hooks
rust
use std::panic;
fn main() {
// Custom panic handler
panic::set_hook(Box::new(|panic_info| {
// Log to monitoring system
if let Some(location) = panic_info.location() {
eprintln!(
"Panic at {}:{}",
location.file(),
location.line()
);
}
if let Some(message) = panic_info.payload().downcast_ref::<&str>() {
eprintln!("Message: {}", message);
}
}));
panic!("custom panic");
}Error Handling Patterns
Layer-based Error Handling
rust
// === DOMAIN LAYER ===
#[derive(Error, Debug)]
enum DomainError {
#[error("User not found: {0}")]
UserNotFound(u32),
#[error("Invalid email format")]
InvalidEmail,
}
// === SERVICE LAYER ===
#[derive(Error, Debug)]
enum ServiceError {
#[error("Domain: {0}")]
Domain(#[from] DomainError),
#[error("Database: {0}")]
Database(#[from] sqlx::Error),
}
// === API LAYER ===
#[derive(Error, Debug)]
enum ApiError {
#[error("Service: {0}")]
Service(#[from] ServiceError),
#[error("Unauthorized")]
Unauthorized,
}
impl ApiError {
fn status_code(&self) -> u16 {
match self {
ApiError::Unauthorized => 401,
ApiError::Service(ServiceError::Domain(DomainError::UserNotFound(_))) => 404,
_ => 500,
}
}
}Early Return Pattern
rust
fn process(data: &str) -> Result<Output, Error> {
// Validate early
let data = data.trim();
if data.is_empty() {
return Err(Error::EmptyInput);
}
// Parse with ?
let parsed = parse(data)?;
// Transform with ?
let validated = validate(parsed)?;
// Final operation
Ok(compute(validated))
}Fallback Pattern
rust
async fn get_config() -> Config {
// Try primary source
if let Ok(config) = fetch_from_server().await {
return config;
}
// Fallback to local cache
if let Ok(config) = read_local_cache() {
return config;
}
// Last resort: defaults
Config::default()
}Best Practices
✅ Nên làm
rust
// 1. Use meaningful error types
fn fetch_user(id: u32) -> Result<User, FetchUserError> { ... }
// 2. Propagate with ?
fn process() -> Result<(), Error> {
let data = fetch()?;
let parsed = parse(data)?;
Ok(())
}
// 3. Add context
use anyhow::Context;
fn read_config() -> anyhow::Result<Config> {
let contents = std::fs::read_to_string("config.toml")
.context("Failed to read config file")?;
toml::from_str(&contents)
.context("Failed to parse config")
}❌ Không nên làm
rust
// 1. Không dùng unwrap() trong production
// let value = risky().unwrap(); // ❌ Panics on error
// 2. Không ignore errors
// let _ = risky(); // ❌ Error silently ignored
// 3. Không panic cho recoverable errors
// if !valid { panic!("invalid"); } // ❌ Use Result insteadBảng Tóm tắt
rust
// === RESULT ===
fn fallible() -> Result<T, E> { ... }
result? // Propagate error
result.map(f) // Transform Ok
result.map_err(f) // Transform Err
result.and_then(f) // Chain fallible ops
result.unwrap_or(default) // With default
// === OPTION ===
fn maybe() -> Option<T> { ... }
option? // In function returning Option
option.map(f) // Transform Some
option.and_then(f) // Chain
option.unwrap_or(default) // With default
option.ok_or(error) // Convert to Result
// === CUSTOM ERRORS ===
#[derive(thiserror::Error, Debug)]
enum MyError {
#[error("message")]
Variant,
#[error("{0}")]
Wrapped(#[from] OtherError),
}
// === PANIC ===
panic!("message") // Explicit panic
unreachable!() // For impossible cases
unimplemented!() // Placeholder