# Rust Error Handling in 2026: Result, Option, thiserror and anyhow > Master Rust error handling with Result, Option, the ? operator, thiserror for libraries, and anyhow for applications. Practical patterns for 2026. - Published: 2026-07-14 - Updated: 2026-07-14 - Author: SharpSkill - Tags: rust, error-handling, thiserror, anyhow, result, option - Reading time: 9 min --- Rust error handling centers on two enums—`Result` and `Option`—that force explicit handling of success, failure, and absence at compile time. Unlike exceptions that propagate invisibly, Rust's approach makes error paths visible in function signatures, eliminating entire categories of runtime surprises. The `?` operator, combined with crates like `thiserror` and `anyhow`, streamlines this explicit model without sacrificing clarity. > **When to Use Which** > > Use `Option` for values that may legitimately be absent (config fields, search results). Use `Result` when operations can fail with meaningful error information (file I/O, network requests, parsing). ## Understanding Result and Option Fundamentals `Result` represents either success (`Ok(T)`) or failure (`Err(E)`). `Option` represents either a value (`Some(T)`) or absence (`None`). Both are sum types—the compiler ensures every variant gets handled. ```rust // error_examples.rs // Demonstrates Result and Option basic patterns fn find_user(id: u64) -> Option { // Returns None if user doesn't exist if id == 0 { None } else { Some(format!("User-{}", id)) } } fn parse_port(s: &str) -> Result { // Returns Err if parsing fails s.parse::() } fn main() { // Option handling - must address None case match find_user(42) { Some(name) => println!("Found: {}", name), None => println!("User not found"), } // Result handling - must address Err case match parse_port("8080") { Ok(port) => println!("Port: {}", port), Err(e) => println!("Invalid port: {}", e), } } ``` The compiler rejects code that ignores these return values without explicit handling. This design catches bugs at compile time that would surface as null pointer exceptions or uncaught errors in other languages. ## The Question Mark Operator for Concise Propagation The `?` operator transforms verbose `match` chains into readable linear code. When applied to a `Result`, it returns early with the error if present, or unwraps the success value. The same applies to `Option`. ```rust // file_reader.rs // Using ? for clean error propagation use std::fs::File; use std::io::{self, BufRead, BufReader}; fn read_first_line(path: &str) -> Result { let file = File::open(path)?; // Returns early if open fails let mut reader = BufReader::new(file); let mut line = String::new(); reader.read_line(&mut line)?; // Returns early if read fails Ok(line.trim().to_string()) } fn get_port_from_config(path: &str) -> Result> { let content = read_first_line(path)?; let port = content.parse::()?; // ParseIntError converts via From Ok(port) } ``` The `?` operator requires the error type to be convertible to the function's return error type via the `From` trait. Using `Box` as shown above accepts any error type implementing the standard [Error trait](https://doc.rust-lang.org/std/error/trait.Error.html). ## Creating Custom Errors with thiserror The [thiserror](https://docs.rs/thiserror/latest/thiserror/) crate eliminates boilerplate for custom error types. It derives `Error`, `Display`, and `From` implementations through a procedural macro. ```rust // errors.rs // Custom error types using thiserror 2.0 use thiserror::Error; #[derive(Error, Debug)] pub enum ConfigError { #[error("configuration file not found at {path}")] NotFound { path: String }, #[error("invalid port number: {0}")] InvalidPort(#[from] std::num::ParseIntError), #[error("IO error reading config")] IoError(#[from] std::io::Error), #[error("missing required field: {0}")] MissingField(String), } fn load_config(path: &str) -> Result { let content = std::fs::read_to_string(path)?; // IoError auto-converts let port: u16 = content .lines() .find(|l| l.starts_with("port=")) .ok_or(ConfigError::MissingField("port".into()))? .strip_prefix("port=") .unwrap() .parse()?; // ParseIntError auto-converts to InvalidPort Ok(Config { port }) } struct Config { port: u16, } ``` The `#[from]` attribute generates automatic conversions, enabling seamless use of `?` with different underlying error types. Error messages become self-documenting through the `#[error(...)]` format strings. ## Application-Level Errors with anyhow While `thiserror` suits library code with specific error types, [anyhow](https://docs.rs/anyhow/latest/anyhow/) targets applications where error context matters more than type granularity. Its `Context` trait adds descriptive messages to any error. ```rust // main.rs // Application error handling with anyhow 1.0 use anyhow::{Context, Result, bail, ensure}; fn load_database_url() -> Result { std::env::var("DATABASE_URL") .context("DATABASE_URL environment variable not set") } fn connect_to_database(url: &str) -> Result { ensure!(!url.is_empty(), "database URL cannot be empty"); let conn = DatabaseConnection::new(url) .context("failed to establish database connection")?; if !conn.is_healthy() { bail!("database connection unhealthy after establishment"); } Ok(conn) } fn main() -> Result<()> { let url = load_database_url()?; let conn = connect_to_database(&url) .context("application startup failed")?; // Context chains create readable error traces: // Error: application startup failed // Caused by: // 0: failed to establish database connection // 1: connection refused Ok(()) } struct DatabaseConnection; impl DatabaseConnection { fn new(_url: &str) -> Result { Ok(Self) } fn is_healthy(&self) -> bool { true } } ``` The `context()` method wraps errors with additional information, creating a chain that aids debugging. The `bail!` macro provides early exit with a formatted error message, while `ensure!` acts as an assertion that returns an error instead of panicking. ## Combining thiserror and anyhow in Real Projects Libraries expose structured errors via `thiserror` for programmatic handling by consumers. Applications wrap those errors with `anyhow` for human-readable output. This separation keeps APIs clean while maintaining debuggability. ```rust // lib.rs - Library code with thiserror // Exposes typed errors for programmatic handling use thiserror::Error; #[derive(Error, Debug)] pub enum PaymentError { #[error("insufficient funds: required {required}, available {available}")] InsufficientFunds { required: u64, available: u64 }, #[error("card declined: {reason}")] CardDeclined { reason: String }, #[error("payment provider unavailable")] ProviderUnavailable(#[source] reqwest::Error), } pub fn process_payment(amount: u64) -> Result { // Library returns specific, matchable error types Err(PaymentError::InsufficientFunds { required: amount, available: 50, }) } pub struct Receipt; ``` ```rust // main.rs - Application code with anyhow // Wraps library errors with context use anyhow::{Context, Result}; use my_payment_lib::{process_payment, PaymentError}; fn checkout(cart_total: u64) -> Result<()> { match process_payment(cart_total) { Ok(_receipt) => Ok(()), Err(PaymentError::InsufficientFunds { required, available }) => { // Handle specific case differently println!("Add {} to your balance", required - available); Ok(()) } Err(e) => Err(e).context("checkout payment processing failed"), } } ``` This pattern enables callers to match on specific variants when recovery is possible, while still benefiting from rich error context when propagating failures upward. The Rust community has largely standardized on this approach, as discussed in the [Rust API guidelines](https://rust-lang.github.io/api-guidelines/interoperability.html#error-types-are-meaningful-and-well-behaved-c-good-err). ## Error Handling Patterns for Async Code Async functions return `Result` just like synchronous ones. The `?` operator works identically within async blocks, and both `thiserror` and `anyhow` integrate without modification. ```rust // async_errors.rs // Error handling in async Rust with Tokio use anyhow::{Context, Result}; use std::time::Duration; async fn fetch_user_data(user_id: u64) -> Result { let response = reqwest::get(format!("https://api.example.com/users/{}", user_id)) .await .context("HTTP request to user API failed")?; let status = response.status(); if !status.is_success() { anyhow::bail!("user API returned status {}", status); } let data: UserData = response .json() .await .context("failed to parse user data JSON")?; Ok(data) } async fn fetch_with_retry(user_id: u64, attempts: u32) -> Result { let mut last_error = None; for attempt in 1..=attempts { match fetch_user_data(user_id).await { Ok(data) => return Ok(data), Err(e) => { last_error = Some(e); if attempt < attempts { tokio::time::sleep(Duration::from_millis(100 * attempt as u64)).await; } } } } Err(last_error.unwrap()).context(format!("failed after {} attempts", attempts)) } #[derive(serde::Deserialize)] struct UserData { name: String, } ``` When combining multiple async operations, use `try_join!` from [tokio](https://docs.rs/tokio/latest/tokio/macro.try_join.html) or [futures](https://docs.rs/futures/latest/futures/macro.try_join.html) to run them concurrently while propagating the first error. For related concepts, explore the [async/await interview questions](/technologies/rust/interview-questions/async-await) module. ## Downcasting and Error Inspection Both `anyhow::Error` and `Box` support downcasting to recover the original error type. This enables logging specific details while still propagating generic errors. ```rust // downcasting.rs // Inspecting wrapped error types use anyhow::{Context, Result}; use std::io; fn log_and_propagate(result: Result<()>) -> Result<()> { if let Err(ref e) = result { // Check if the root cause is a specific type if let Some(io_err) = e.downcast_ref::() { match io_err.kind() { io::ErrorKind::NotFound => { tracing::warn!("file not found, using defaults"); } io::ErrorKind::PermissionDenied => { tracing::error!("permission denied - check file ownership"); } _ => { tracing::error!("IO error: {:?}", io_err); } } } } result } ``` Downcasting bridges the gap between generic error handling and specific recovery logic. Use it sparingly—if frequent downcasting occurs, consider whether a typed error enum would serve better. ## Converting Between Option and Result The standard library provides methods to convert between `Option` and `Result`, enabling smooth composition when different APIs use different patterns. ```rust // conversions.rs // Option and Result interoperability fn get_env_port() -> Option { std::env::var("PORT") .ok() // Result -> Option (discards error) .and_then(|s| s.parse().ok()) } fn get_env_port_with_error() -> Result { std::env::var("PORT") .map_err(|_| "PORT not set".to_string())? .parse() .map_err(|_| "PORT is not a valid number".to_string()) } fn lookup_and_parse(map: &std::collections::HashMap, key: &str) -> Result { map.get(key) .ok_or_else(|| format!("key '{}' not found", key))? // Option -> Result .parse() .map_err(|e| format!("parse error for '{}': {}", key, e)) } ``` The `ok()` method discards error details when only presence matters. The `ok_or()` and `ok_or_else()` methods convert `None` into a custom error, enabling `?` propagation from `Option` values. These patterns appear frequently in [pattern matching interview questions](/technologies/rust/interview-questions/pattern-matching). ## Performance Considerations Error handling in Rust carries no runtime cost on the success path. `Result` and `Option` are stack-allocated enums with deterministic size. The compiler optimizes away checks when it can prove a branch is unreachable. | Approach | Success Path Cost | Failure Path Cost | |----------|------------------|-------------------| | Result/Option | Zero | Stack unwinding (cheap) | | panic! | Zero | Full stack unwinding + cleanup | | C++ exceptions | Zero (usually) | Expensive heap allocation + RTTI | Avoid `unwrap()` and `expect()` in library code—reserve them for cases where failure genuinely indicates a bug. For performance-critical paths where errors are common, consider using enums with inline data rather than heap-allocated error types. ## Conclusion - `Result` handles recoverable failures; `Option` handles absence—both enforce compile-time handling - The `?` operator propagates errors concisely, requiring `From` trait implementations for type conversion - `thiserror` generates structured error types for libraries with zero boilerplate - `anyhow` provides contextual error chains for applications, supporting `context()`, `bail!`, and `ensure!` - Combine both: libraries expose typed errors via `thiserror`, applications wrap them with `anyhow` - Async code uses identical patterns—`?` works in async blocks without modification - Use `ok_or()` to convert `Option` to `Result`; use `ok()` for the reverse - Downcast wrapped errors only when specific recovery logic requires the original type --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/rust/rust-error-handling-result-option-thiserror-anyhow