**** 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 **
| Java | try-catch | checked | |
| Python | try-except | ||
| Go | if err != nil | ||
| Rust | Result<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(())
}