# Error Handling: Library vs Application ## Library Error Design ### Principles 1. **Define specific error types** - Don't use `anyhow` in libraries 2. **Implement std::error::Error** - For compatibility 3. **Provide error variants** - Let users match on errors 4. **Include source errors** - Enable error chains 5. **Be `Send + Sync`** - For async compatibility ### Example: Library Error Type ```rust // lib.rs use thiserror::Error; #[derive(Error, Debug)] pub enum DatabaseError { #[error("connection failed: {host}:{port}")] ConnectionFailed { host: String, port: u16, #[source] source: std::io::Error, }, #[error("query failed: {query}")] QueryFailed { query: String, #[source] source: SqlError, }, #[error("record not found: {table}.{id}")] NotFound { table: String, id: String }, #[error("constraint violation: {0}")] ConstraintViolation(String), } // Public Result alias pub type Result = std::result::Result; // Library functions pub fn connect(host: &str, port: u16) -> Result { // ... } pub fn query(conn: &Connection, sql: &str) -> Result { // ... } ``` ### Library Usage of Errors ```rust impl Database { pub fn get_user(&self, id: &str) -> Result { let rows = self.query(&format!("SELECT * FROM users WHERE id = '{}'", id))?; rows.first() .cloned() .ok_or_else(|| DatabaseError::NotFound { table: "users".to_string(), id: id.to_string(), }) } } ``` --- ## Application Error Design ### Principles 1. **Use anyhow for convenience** - Or custom unified error 2. **Add context liberally** - Help debugging 3. **Log at boundaries** - Don't log in libraries 4. **Convert to user-friendly messages** - For display ### Example: Application Error Handling ```rust // main.rs use anyhow::{Context, Result}; use tracing::{error, info}; async fn run_server() -> Result<()> { let config = load_config() .context("failed to load configuration")?; let db = Database::connect(&config.db_url) .await .context("failed to connect to database")?; let server = Server::new(config.port) .context("failed to create server")?; info!("Server starting on port {}", config.port); server.run(db).await .context("server error")?; Ok(()) } #[tokio::main] async fn main() { tracing_subscriber::init(); if let Err(e) = run_server().await { error!("Application error: {:#}", e); std::process::exit(1); } } ``` ### Converting Library Errors ```rust use mylib::DatabaseError; async fn get_user_handler(id: &str) -> Result { match db.get_user(id).await { Ok(user) => Ok(Response::json(user)), Err(DatabaseError::NotFound { .. }) => { Ok(Response::not_found("User not found")) } Err(DatabaseError::ConnectionFailed { .. }) => { error!("Database connection failed"); Ok(Response::internal_error("Service unavailable")) } Err(e) => { error!("Database error: {}", e); Err(e.into()) // Convert to anyhow::Error } } } ``` --- ## Error Handling Layers ``` ┌─────────────────────────────────────┐ │ Application Layer │ │ - Use anyhow or unified error │ │ - Add context at boundaries │ │ - Log errors │ │ - Convert to user messages │ └─────────────────────────────────────┘ │ │ calls ▼ ┌─────────────────────────────────────┐ │ Service Layer │ │ - Map between error types │ │ - Add business context │ │ - Handle recoverable errors │ └─────────────────────────────────────┘ │ │ calls ▼ ┌─────────────────────────────────────┐ │ Library Layer │ │ - Define specific error types │ │ - Use thiserror │ │ - Include source errors │ │ - No logging │ └─────────────────────────────────────┘ ``` --- ## Practical Examples ### HTTP API Error Response ```rust use axum::{response::IntoResponse, http::StatusCode}; use serde::Serialize; #[derive(Serialize)] struct ErrorResponse { error: String, code: String, } enum AppError { NotFound(String), BadRequest(String), Internal(anyhow::Error), } impl IntoResponse for AppError { fn into_response(self) -> axum::response::Response { let (status, error, code) = match self { AppError::NotFound(msg) => { (StatusCode::NOT_FOUND, msg, "NOT_FOUND") } AppError::BadRequest(msg) => { (StatusCode::BAD_REQUEST, msg, "BAD_REQUEST") } AppError::Internal(e) => { tracing::error!("Internal error: {:#}", e); ( StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string(), "INTERNAL_ERROR", ) } }; let body = ErrorResponse { error, code: code.to_string(), }; (status, axum::Json(body)).into_response() } } ``` ### CLI Error Handling ```rust use anyhow::{Context, Result}; use clap::Parser; #[derive(Parser)] struct Args { #[arg(short, long)] config: String, } fn main() { if let Err(e) = run() { eprintln!("Error: {:#}", e); std::process::exit(1); } } fn run() -> Result<()> { let args = Args::parse(); let config = std::fs::read_to_string(&args.config) .context(format!("Failed to read config file: {}", args.config))?; let parsed: Config = toml::from_str(&config) .context("Failed to parse config file")?; process(parsed)?; println!("Done!"); Ok(()) } ``` --- ## Testing Error Handling ### Testing Error Cases ```rust #[cfg(test)] mod tests { use super::*; #[test] fn test_not_found_error() { let result = db.get_user("nonexistent"); assert!(matches!( result, Err(DatabaseError::NotFound { table, id }) if table == "users" && id == "nonexistent" )); } #[test] fn test_error_message() { let err = DatabaseError::NotFound { table: "users".to_string(), id: "123".to_string(), }; assert_eq!(err.to_string(), "record not found: users.123"); } #[test] fn test_error_chain() { let io_err = std::io::Error::new( std::io::ErrorKind::ConnectionRefused, "connection refused" ); let err = DatabaseError::ConnectionFailed { host: "localhost".to_string(), port: 5432, source: io_err, }; // Check source is preserved assert!(err.source().is_some()); } } ``` ### Testing with anyhow ```rust #[cfg(test)] mod tests { use super::*; #[test] fn test_with_context() -> anyhow::Result<()> { let result = process("valid input")?; assert_eq!(result, expected); Ok(()) } #[test] fn test_error_context() { let err = process("invalid") .context("processing failed") .unwrap_err(); // Check error chain contains expected text let chain = format!("{:#}", err); assert!(chain.contains("processing failed")); } } ```