Robust error handling and well-designed patterns are essential for production Rust applications. This chapter covers advanced error handling techniques and common design patterns.
use std::fmt;
#[derive(Debug)]
enum DatabaseError {
ConnectionFailed(String),
QueryFailed(String),
NotFound,
}
impl fmt::Display for DatabaseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
DatabaseError::ConnectionFailed(msg) => {
write!(f, "Connection failed: {}", msg)
}
DatabaseError::QueryFailed(msg) => {
write!(f, "Query failed: {}", msg)
}
DatabaseError::NotFound => {
write!(f, "Record not found")
}
}
}
}
impl std::error::Error for DatabaseError {}
fn query_database(id: u64) -> Result<String, DatabaseError> {
if id == 0 {
Err(DatabaseError::NotFound)
} else {
Ok(format!("User {}", id))
}
}
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("Database error: {0}")]
Database(#[from] DatabaseError),
#[error("IO error")]
Io(#[from] std::io::Error),
#[error("Invalid configuration: {field}")]
InvalidConfig { field: String },
#[error("User {id} not found")]
UserNotFound { id: u64 },
}
fn load_user(id: u64) -> Result<String, AppError> {
if id == 0 {
return Err(AppError::UserNotFound { id });
}
Ok(format!("User {}", id))
}
use anyhow::{Context, Result, bail};
fn read_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.context("Failed to read config file")?;
let config: Config = serde_json::from_str(&content)
.context("Failed to parse config")?;
if config.port == 0 {
bail!("Invalid port number");
}
Ok(config)
}
#[derive(serde::Deserialize)]
struct Config {
port: u16,
host: String,
}
trait ResultExt<T> {
fn with_context<F, S>(self, f: F) -> Result<T, String>
where
F: FnOnce() -> S,
S: Into<String>;
}
impl<T, E: fmt::Display> ResultExt<T> for Result<T, E> {
fn with_context<F, S>(self, f: F) -> Result<T, String>
where
F: FnOnce() -> S,
S: Into<String>,
{
self.map_err(|e| format!("{}: {}", f().into(), e))
}
}
fn example() -> Result<(), String> {
let content = std::fs::read_to_string("config.json")
.with_context(|| "Failed to read config")?;
Ok(())
}
struct HttpClient {
base_url: String,
timeout: std::time::Duration,
headers: Vec<(String, String)>,
retry_count: u32,
}
struct HttpClientBuilder {
base_url: Option<String>,
timeout: std::time::Duration,
headers: Vec<(String, String)>,
retry_count: u32,
}
impl HttpClientBuilder {
fn new() -> Self {
Self {
base_url: None,
timeout: std::time::Duration::from_secs(30),
headers: Vec::new(),
retry_count: 3,
}
}
fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = Some(url.into());
self
}
fn timeout(mut self, duration: std::time::Duration) -> Self {
self.timeout = duration;
self
}
fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((key.into(), value.into()));
self
}
fn retry_count(mut self, count: u32) -> Self {
self.retry_count = count;
self
}
fn build(self) -> Result<HttpClient, String> {
Ok(HttpClient {
base_url: self.base_url.ok_or("base_url is required")?,
timeout: self.timeout,
headers: self.headers,
retry_count: self.retry_count,
})
}
}
fn main() {
let client = HttpClientBuilder::new()
.base_url("https://api.example.com")
.timeout(std::time::Duration::from_secs(10))
.header("User-Agent", "MyApp/1.0")
.retry_count(5)
.build()
.unwrap();
}
use std::marker::PhantomData;
// States
struct Created;
struct Connected;
struct Authenticated;
struct Database<State = Created> {
connection_string: String,
_state: PhantomData<State>,
}
impl Database<Created> {
fn new(connection_string: String) -> Self {
Database {
connection_string,
_state: PhantomData,
}
}
fn connect(self) -> Result<Database<Connected>, String> {
println!("Connecting to {}", self.connection_string);
Ok(Database {
connection_string: self.connection_string,
_state: PhantomData,
})
}
}
impl Database<Connected> {
fn authenticate(self, password: &str) -> Result<Database<Authenticated>, String> {
if password == "secret" {
Ok(Database {
connection_string: self.connection_string,
_state: PhantomData,
})
} else {
Err("Authentication failed".to_string())
}
}
}
impl Database<Authenticated> {
fn query(&self, sql: &str) -> Result<Vec<String>, String> {
println!("Executing: {}", sql);
Ok(vec!["result1".to_string(), "result2".to_string()])
}
}
fn main() {
let db = Database::new("localhost:5432".to_string())
.connect()
.unwrap()
.authenticate("secret")
.unwrap();
db.query("SELECT * FROM users").unwrap();
// Won't compile: can't query before authentication!
// let db = Database::new("localhost:5432".to_string());
// db.query("SELECT * FROM users");
}
trait CompressionStrategy {
fn compress(&self, data: &[u8]) -> Vec<u8>;
fn decompress(&self, data: &[u8]) -> Vec<u8>;
}
struct GzipCompression;
struct ZstdCompression;
impl CompressionStrategy for GzipCompression {
fn compress(&self, data: &[u8]) -> Vec<u8> {
println!("Compressing with gzip");
data.to_vec() // Simplified
}
fn decompress(&self, data: &[u8]) -> Vec<u8> {
println!("Decompressing with gzip");
data.to_vec()
}
}
impl CompressionStrategy for ZstdCompression {
fn compress(&self, data: &[u8]) -> Vec<u8> {
println!("Compressing with zstd");
data.to_vec()
}
fn decompress(&self, data: &[u8]) -> Vec<u8> {
println!("Decompressing with zstd");
data.to_vec()
}
}
struct Compressor {
strategy: Box<dyn CompressionStrategy>,
}
impl Compressor {
fn new(strategy: Box<dyn CompressionStrategy>) -> Self {
Compressor { strategy }
}
fn compress(&self, data: &[u8]) -> Vec<u8> {
self.strategy.compress(data)
}
}
fn main() {
let data = b"Hello, World!";
let compressor = Compressor::new(Box::new(GzipCompression));
compressor.compress(data);
let compressor = Compressor::new(Box::new(ZstdCompression));
compressor.compress(data);
}
trait Command {
fn execute(&mut self) -> Result<(), String>;
fn undo(&mut self) -> Result<(), String>;
}
struct Counter {
value: i32,
}
struct IncrementCommand {
counter: *mut Counter,
amount: i32,
}
impl Command for IncrementCommand {
fn execute(&mut self) -> Result<(), String> {
unsafe {
(*self.counter).value += self.amount;
}
Ok(())
}
fn undo(&mut self) -> Result<(), String> {
unsafe {
(*self.counter).value -= self.amount;
}
Ok(())
}
}
struct CommandHistory {
commands: Vec<Box<dyn Command>>,
position: usize,
}
impl CommandHistory {
fn new() -> Self {
CommandHistory {
commands: Vec::new(),
position: 0,
}
}
fn execute(&mut self, mut command: Box<dyn Command>) -> Result<(), String> {
command.execute()?;
self.commands.truncate(self.position);
self.commands.push(command);
self.position += 1;
Ok(())
}
fn undo(&mut self) -> Result<(), String> {
if self.position == 0 {
return Err("Nothing to undo".to_string());
}
self.position -= 1;
self.commands[self.position].undo()
}
}
trait Visitor {
fn visit_file(&mut self, file: &File);
fn visit_directory(&mut self, dir: &Directory);
}
trait Visitable {
fn accept(&self, visitor: &mut dyn Visitor);
}
struct File {
name: String,
size: u64,
}
struct Directory {
name: String,
children: Vec<Box<dyn Visitable>>,
}
impl Visitable for File {
fn accept(&self, visitor: &mut dyn Visitor) {
visitor.visit_file(self);
}
}
impl Visitable for Directory {
fn accept(&self, visitor: &mut dyn Visitor) {
visitor.visit_directory(self);
for child in &self.children {
child.accept(visitor);
}
}
}
struct SizeCalculator {
total_size: u64,
}
impl Visitor for SizeCalculator {
fn visit_file(&mut self, file: &File) {
self.total_size += file.size;
}
fn visit_directory(&mut self, _dir: &Directory) {
// Directory itself has no size
}
}
trait Logger {
fn log(&self, message: &str);
}
struct ConsoleLogger;
struct FileLogger {
path: String,
}
impl Logger for ConsoleLogger {
fn log(&self, message: &str) {
println!("[LOG] {}", message);
}
}
impl Logger for FileLogger {
fn log(&self, message: &str) {
println!("[FILE] Writing to {}: {}", self.path, message);
}
}
struct UserService<L: Logger> {
logger: L,
}
impl<L: Logger> UserService<L> {
fn new(logger: L) -> Self {
UserService { logger }
}
fn create_user(&self, name: &str) {
self.logger.log(&format!("Creating user: {}", name));
// Create user logic
}
}
fn main() {
let console_service = UserService::new(ConsoleLogger);
console_service.create_user("Alice");
let file_service = UserService::new(FileLogger {
path: "app.log".to_string(),
});
file_service.create_user("Bob");
}
fn combinator_examples() {
// map: transform the value
let value = Some(5).map(|x| x * 2); // Some(10)
// and_then: chain operations
let result = Some(5)
.and_then(|x| if x > 0 { Some(x * 2) } else { None });
// or: provide alternative
let value = None.or(Some(42)); // Some(42)
// unwrap_or: default value
let value = None.unwrap_or(0); // 0
// map_or: transform with default
let value = Some("hello").map_or(0, |s| s.len()); // 5
// ok_or: convert Option to Result
let result: Result<i32, &str> = Some(42).ok_or("not found");
}
fn result_combinators() -> Result<i32, String> {
// Chaining with ?
let value = parse_number("42")?;
let doubled = multiply_by_two(value)?;
Ok(doubled)
}
fn parse_number(s: &str) -> Result<i32, String> {
s.parse().map_err(|e| format!("Parse error: {}", e))
}
fn multiply_by_two(n: i32) -> Result<i32, String> {
Ok(n * 2)
}