Skip to content

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&lt;T, E&gt; 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&lt;T&gt; 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 abort

Unwinding (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 instead

Bả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