**** IP internal error public error

**** IO Result unwrap() expect() panic set_hook

**** thiserror anyhow Box<dyn Error> anyhow::Error

**** → → →

:
:


Rust Result<T, E> panic!

** Java/Python/Go **

Javatry-catchchecked
Pythontry-except
Goif err != nil
RustResult<T, E> + ?

**Rust **

//  — 
let file = File::open("config.toml");  //  Result<File, Error>
 
// 
// 1. ? 
fn read_config() -> Result<Config, Box<dyn Error>> {
    let file = File::open("config.toml")?;  // 
    // ...
    Ok(config)
}
 
// 2. unwrap/expect — 
let file = File::open("config.toml").expect("config file must exist");
 
// 3. match 
let file = match File::open("config.toml") {
    Ok(f) => f,
    Err(e) => {
        eprintln!("failed to open config: {}", e);
        return Err(e.into());
    }
};
 
// 4. 
let file = File::open("config.toml")
    .map_err(|e| format!("open failed: {}", e))?
    .metadata()?
    .len();

thiserror —

thiserror std::error::Error trait

use thiserror::Error;
 
#[derive(Error, Debug)]
pub enum ServiceError {
    //  — 
    #[error("authentication failed for user: {username}")]
    AuthFailed {
        username: String,
        reason: String,
    },
 
    // #[from]
    #[error("database error: {0}")]
    Database(#[from] sqlx::Error),
 
    // 
    #[error("configuration error: {0}")]
    Config(#[from] config::ConfigError),
 
    //  #[from]
    #[error("validation failed: {field} - {message}")]
    Validation {
        field: String,
        message: String,
    },
 
    // 
    #[error("resource not found: {resource} with id {id}")]
    NotFound {
        resource: String,
        id: String,
    },
 
    //  — 
    #[error("internal error (ref: {ref_id})")]
    Internal {
        ref_id: String,
        #[source]  // 
        source: anyhow::Error,
    },
 
    // 
    #[error("operation timed out after {seconds}s")]
    Timeout { seconds: u64 },
 
    // 
    #[error("permission denied: requires {required_permission}")]
    PermissionDenied {
        required_permission: String,
    },
}
 
// 
impl From<&str> for ServiceError {
    fn from(s: &str) -> Self {
        ServiceError::Validation {
            field: "unknown".into(),
            message: s.into(),
        }
    }
}

**thiserror **

#[error("msg")]Display#[error("failed to read {path}")]
#[from]From<E>#[from] sqlx::Error
#[source]#[source] anyhow::Error
#[transparent]Display/Source

//  
#[derive(Error, Debug)]
pub enum ParseError {
    #[error("unexpected token: expected {expected}, got {got}")]
    UnexpectedToken { expected: String, got: String },
 
    #[error("unexpected end of input")]
    UnexpectedEof,
 
    #[error("invalid number: {0}")]
    InvalidNumber(String),
}
 
//   std::error::Error::source() 
#[derive(Error, Debug)]
pub enum StorageError {
    #[error("IO error while {operation}")]
    Io {
        operation: String,
        #[source] source: std::io::Error,
    },
 
    #[error("serialization failed")]
    Serialization(#[from] serde_json::Error),
}
 
//  
#[derive(Error, Debug)]
pub enum BadError {
    #[error("column 'users.email' violates unique constraint at line 42")]
    DbInternal { sql_state: String, line: u32 },  //  DB 
}

anyhow —

anyhow

use anyhow::{anyhow, bail, ensure, Context, Result};
 
// anyhow::Result = Result<T, anyhow::Error>
 
fn read_config(path: &str) -> Result<Config> {
    // Context trait —  Result 
    let content = std::fs::read_to_string(path)
        .context(format!("failed to read config from {}", path))?;
 
    // anyhow! — 
    let config: Config = toml::from_str(&content)
        .map_err(|e| anyhow!("failed to parse config: {}", e))?;
 
    // bail! — 
    if config.name.is_empty() {
        bail!("config name cannot be empty");
    }
 
    // ensure! — 
    ensure!(
        config.port > 0 && config.port < 65535,
        "invalid port: {}",
        config.port
    );
 
    Ok(config)
}
 
// 
fn process_data(input: &str) -> Result<String> {
    let parsed = parse_input(input)
        .context("failed to parse input")?;
 
    let validated = validate(parsed)
        .context("validation failed")?;
 
    let transformed = transform(validated)
        .context("transformation failed")?;
 
    Ok(transformed)
}
 
// anyhow 
fn multi_error() -> Result<()> {
    let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
    let parse_err = "not a number".parse::<i32>().unwrap_err();
 
    //  std::error::Error 
    Err(io_err.into())
}

anyhow API

API
.context(msg)Result
anyhow!(msg)
bail!(msg)return Err(anyhow!(msg))
ensure!(cond, msg)
anyhow::Error::downcast_ref::<T>()

// IO 
fn read_file(path: &str) -> Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}
 
// 
fn parse_config(path: &str) -> Result<Config, anyhow::Error> {
    let content = read_file(path)
        .context(format!("reading config file: {}", path))?;
    // : "reading config file: config.toml: No such file or directory"
 
    let config: Config = toml::from_str(&content)
        .context("parsing config")?;
    // : "reading config file: ... -> parsing config: invalid TOML"
 
    Ok(config)
}
 
// 
fn init_app() -> Result<(), anyhow::Error> {
    let config = parse_config("config.toml")
        .context("application initialization")?;
    // 
    Ok(())
}

use anyhow::Result;
 
fn example() -> Result<()> {
    let outer = read_file("missing.txt")
        .context("outer context")?;
 
    Ok(())
}
 
// :
// outer context: No such file or directory (os error 2)
 
// 
fn inspect_error_chain(err: &anyhow::Error) {
    // 
    println!("Error: {}", err);
 
    // 
    if let Some(source) = err.source() {
        println!("Caused by: {}", source);
    }
 
    // 
    for cause in err.chain() {
        println!("  -> {}", cause);
    }
}

panic vs Result

panic

panic! ——

// 1. 
fn get_element(index: usize, data: &[i32]) -> i32 {
    assert!(index < data.len(), "index out of bounds: {} >= {}", index, data.len());
    data[index]
}
 
// 2. 
fn init_database() -> Database {
    let url = std::env::var("DATABASE_URL")
        .expect("DATABASE_URL environment variable must be set");
    Database::connect(&url).expect("failed to connect to database")
}
 
// 3. 
let arr = [1, 2, 3];
// let x = arr[10];  // panic: index out of bounds
 
// 4. unwrap() 
fn read_u32(s: &str) -> u32 {
    //  s 
    s.parse::<u32>().unwrap()  //  panic bug
}
 
// 5.  panic hook —  panic
use std::panic;
panic::set_hook(Box::new(|info| {
    let location = info.location()
        .map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
        .unwrap_or_else(|| "unknown".to_string());
    let payload = info.payload().downcast_ref::<String>()
        .map(|s| s.as_str())
        .or_else(|| info.payload().downcast_ref::<&str>())
        .unwrap_or("no message");
    tracing::error!(
        panic_location = %location,
        panic_message = %payload,
        "application panic"
    );
    // 
    std::process::abort();
}));

Result

Result ——

// 1. IO 
fn read_user(id: u64) -> Result<User, io::Error> {
    let path = format!("users/{}.json", id);
    let content = std::fs::read_to_string(&path)?;
    let user = serde_json::from_str(&content)?;
    Ok(user)
}
 
// 2. 
async fn fetch_data(url: &str) -> Result<Response, reqwest::Error> {
    reqwest::get(url).await?.json().await
}
 
// 3. /
fn parse_port(s: &str) -> Result<u16, ParseError> {
    let port: u16 = s.parse().map_err(|_| ParseError::InvalidPort(s.to_string()))?;
    if port == 0 {
        return Err(ParseError::InvalidPort("port cannot be 0".into()));
    }
    Ok(port)
}
 
// 4.  —  panic Result 
pub fn process(input: &str) -> Result<Output, Error> {
    let parsed = parse(input)?;
    let validated = validate(parsed)?;
    Ok(Output::from(validated))
}

1:

fn fetch_with_fallback(url: &str) -> Result<String> {
    //  URL
    match reqwest::blocking::get(url) {
        Ok(resp) if resp.status().is_success() => {
            return Ok(resp.text()?);
        }
        Ok(resp) => {
            tracing::warn!("primary returned status {}", resp.status());
        }
        Err(e) => {
            tracing::warn!("primary failed: {}", e);
        }
    }
 
    //  URL
    let fallback_url = url.replace("primary", "fallback");
    let resp = reqwest::blocking::get(&fallback_url)
        .context("both primary and fallback failed")?;
    Ok(resp.text()?)
}

2:

fn validate_user(user: &User) -> Result<(), Vec<ValidationError>> {
    let mut errors = Vec::new();
 
    if user.name.is_empty() {
        errors.push(ValidationError::EmptyName);
    }
    if !user.email.contains('@') {
        errors.push(ValidationError::InvalidEmail);
    }
    if user.age < 0 || user.age > 150 {
        errors.push(ValidationError::InvalidAge);
    }
 
    if errors.is_empty() {
        Ok(())
    } else {
        Err(errors)
    }
}
 
// 
match validate_user(&user) {
    Ok(()) => println!("valid"),
    Err(errors) => {
        for err in errors {
            eprintln!("validation error: {}", err);
        }
    }
}

3:

use anyhow::anyhow;
 
fn handle_error(err: anyhow::Error) {
    // 
    if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
        match io_err.kind() {
            std::io::ErrorKind::NotFound => {
                println!("file not found, creating default");
            }
            std::io::ErrorKind::PermissionDenied => {
                println!("permission denied, check file permissions");
            }
            _ => {
                println!("other IO error: {}", io_err);
            }
        }
    } else if let Some(db_err) = err.downcast_ref::<sqlx::Error>() {
        // 
    } else {
        println!("unknown error: {}", err);
    }
}

4: Error Boundary

// 
#[derive(Error, Debug)]
pub enum ApiError {
    #[error("not found")]
    NotFound,
    #[error("unauthorized")]
    Unauthorized,
    #[error("internal error")]
    Internal(#[from] anyhow::Error),
}
 
//  HTTP 
fn error_to_response(err: &ApiError) -> HttpResponse {
    match err {
        ApiError::NotFound => HttpResponse::NotFound().json("not found"),
        ApiError::Unauthorized => HttpResponse::Unauthorized().json("unauthorized"),
        ApiError::Internal(_) => {
            // 
            HttpResponse::InternalServerError().json("internal server error")
        }
    }
}
 
// 
fn handle_request() -> Result<Response, ApiError> {
    let result = process_request().map_err(|e| {
        tracing::error!("request failed: {:?}", e);  // 
        ApiError::Internal(e)  // 
    })?;
    Ok(result)
}

5:

use std::time::Duration;
 
fn retry_with_backoff<F, T, E>(max_retries: u32, mut f: F) -> Result<T, E>
where
    F: FnMut() -> Result<T, E>,
    E: std::fmt::Display,
{
    let mut last_err = None;
 
    for attempt in 0..=max_retries {
        match f() {
            Ok(val) => return Ok(val),
            Err(e) => {
                tracing::warn!("attempt {} failed: {}", attempt + 1, e);
                last_err = Some(e);
                if attempt < max_retries {
                    let delay = Duration::from_millis(100 * 2u64.pow(attempt));
                    std::thread::sleep(delay);
                }
            }
        }
    }
 
    Err(last_err.unwrap())
}
 
// 
let result = retry_with_backoff(3, || {
    reqwest::blocking::get("https://api.example.com/data")?
        .error_for_status()?
        .json()
});

CWE-209

//  
fn bad_handler(id: u64) -> Result<User, String> {
    let user = db.query(&format!(
        "SELECT * FROM users WHERE id = {}",  //  SQL
    )).map_err(|e| format!(
        "Database error at line 42: {} from table 'users'",  // 
    ))?;
    Ok(user)
}
 
//  
fn safe_handler(id: u64) -> Result<User, ServiceError> {
    let user = db.find_user(id)
        .map_err(|e| {
            // 
            tracing::error!(user_id = id, error = %e, "database query failed");
            // 
            ServiceError::Internal {
                ref_id: uuid::Uuid::new_v4().to_string(),
                source: e.into(),
            }
        })?;
    Ok(user)
}
 
//  
fn validate_password(password: &str) -> Result<(), String> {
    if password.len() < 8 {
        // "must be at least 8 characters" 
        //  "must contain uppercase, lowercase, digit, and symbol"
        return Err("password too short".into());
    }
    Ok(())
}