Rust Error Handling: From Beginner to Production

Rust doesn’t have exceptions. Instead, it uses Result<T, E> for recoverable errors and panic! for unrecoverable ones.

이거 에디팅은 잘되는데 헐랭이다.

rust
use std::fs; use std::io; fn read_config(path: &str) -> Result<String, io::Error> { fs::read_to_string(path) } fn main() { match read_config("config.toml") { Ok(content) => println!("Config loaded: {} bytes", content.len()), Err(e) => eprintln!("Failed to load config: {e}"), } }

The ? Operator

The question mark operator propagates errors up the call stack:

rust
fn parse_port(config: &str) -> Result<u16, Box<dyn std::error::Error>> { let content = fs::read_to_string(config)?; let port: u16 = content .lines() .find(|l| l.starts_with("port")) .ok_or("missing port field")? .split('=') .nth(1) .ok_or("invalid format")? .trim() .parse()?; Ok(port) }

Custom Error Types with thiserror

For libraries, define explicit error types:

rust
use thiserror::Error; #[derive(Error, Debug)] pub enum ApiError { #[error("authentication failed: {0}")] AuthFailed(String), #[error("rate limited, retry after {retry_after}s")] RateLimited { retry_after: u64 }, #[error("resource not found: {resource}/{id}")] NotFound { resource: String, id: String }, #[error("request timeout after {0}ms")] Timeout(u64), #[error(transparent)] Internal(#[from] anyhow::Error), } impl ApiError { pub fn status_code(&self) -> u16 { match self { Self::AuthFailed(_) => 401, Self::RateLimited { .. } => 429, Self::NotFound { .. } => 404, Self::Timeout(_) => 504, Self::Internal(_) => 500, } } }

Application Errors with anyhow

For applications (not libraries), anyhow provides ergonomic error handling with context:

rust
use anyhow::{Context, Result}; async fn sync_user_data(user_id: &str) -> Result<()> { let profile = fetch_profile(user_id) .await .context("failed to fetch user profile")?; let preferences = db::get_preferences(user_id) .await .with_context(|| format!("db lookup failed for user {user_id}"))?; let merged = merge_data(profile, preferences) .context("data merge conflict")?; db::save(merged) .await .context("failed to persist merged data")?; Ok(()) }

Pattern: Error Context Chain

The best production error messages tell a story:

Error: failed to start server Caused by: 0: failed to bind to 0.0.0.0:8080 1: address already in use (os error 98)

Decision Matrix

Scenario Use
Library with public API thiserror + custom enum
Application / binary anyhow + .context()
Quick prototype Box<dyn Error>
Performance-critical path Custom enum (no allocation)
FFI boundary Error codes (integer)

Key Takeaways

  1. Never use .unwrap() in production code — use .expect("reason") at minimum
  2. Add context at every boundary — function calls, I/O, parsing
  3. Libraries expose types, applications consume themthiserror vs anyhow
  4. Errors are data — pattern match, convert, enrich, log

Further reading: Rust Error Handling in 2026 | thiserror docs | anyhow docs

Rust Error Handling: From Beginner to Production

Rust doesn’t have exceptions. Instead, it uses Result<T, E> for recoverable errors and panic! for unrecoverable ones.

rust
use std::fs; use std::io; fn read_config(path: &str) -> Result<String, io::Error> { fs::read_to_string(path) } fn main() { match read_config("config.toml") { Ok(content) => println!("Config loaded: {} bytes", content.len()), Err(e) => eprintln!("Failed to load config: {e}"), } }

The ? Operator

The question mark operator propagates errors up the call stack:

rust
fn parse_port(config: &str) -> Result<u16, Box<dyn std::error::Error>> { let content = fs::read_to_string(config)?; let port: u16 = content .lines() .find(|l| l.starts_with("port")) .ok_or("missing port field")? .split('=') .nth(1) .ok_or("invalid format")? .trim() .parse()?; Ok(port) }

Custom Error Types with thiserror

For libraries, define explicit error types:

rust
use thiserror::Error; #[derive(Error, Debug)] pub enum ApiError { #[error("authentication failed: {0}")] AuthFailed(String), #[error("rate limited, retry after {retry_after}s")] RateLimited { retry_after: u64 }, #[error("resource not found: {resource}/{id}")] NotFound { resource: String, id: String }, #[error("request timeout after {0}ms")] Timeout(u64), #[error(transparent)] Internal(#[from] anyhow::Error), } impl ApiError { pub fn status_code(&self) -> u16 { match self { Self::AuthFailed(_) => 401, Self::RateLimited { .. } => 429, Self::NotFound { .. } => 404, Self::Timeout(_) => 504, Self::Internal(_) => 500, } } }

Application Errors with anyhow

For applications (not libraries), anyhow provides ergonomic error handling with context:

rust
use anyhow::{Context, Result}; async fn sync_user_data(user_id: &str) -> Result<()> { let profile = fetch_profile(user_id) .await .context("failed to fetch user profile")?; let preferences = db::get_preferences(user_id) .await .with_context(|| format!("db lookup failed for user {user_id}"))?; let merged = merge_data(profile, preferences) .context("data merge conflict")?; db::save(merged) .await .context("failed to persist merged data")?; Ok(()) }

Pattern: Error Context Chain

The best production error messages tell a story:

Error: failed to start server Caused by: 0: failed to bind to 0.0.0.0:8080 1: address already in use (os error 98)

Decision Matrix

Scenario Use
Library with public API thiserror + custom enum
Application / binary anyhow + .context()
Quick prototype Box<dyn Error>
Performance-critical path Custom enum (no allocation)
FFI boundary Error codes (integer)

Key Takeaways

  1. Never use .unwrap() in production code — use .expect("reason") at minimum
  2. Add context at every boundary — function calls, I/O, parsing
  3. Libraries expose types, applications consume themthiserror vs anyhow
  4. Errors are data — pattern match, convert, enrich, log

Further reading: Rust Error Handling in 2026 | thiserror docs | anyhow docs