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.
이거 에디팅은 잘되는데 헐랭이다.
rustuse 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:
rustfn 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:
rustuse 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:
rustuse 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
- Never use
.unwrap()in production code — use.expect("reason")at minimum - Add context at every boundary — function calls, I/O, parsing
- Libraries expose types, applications consume them —
thiserrorvsanyhow - 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.
rustuse 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:
rustfn 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:
rustuse 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:
rustuse 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
- Never use
.unwrap()in production code — use.expect("reason")at minimum - Add context at every boundary — function calls, I/O, parsing
- Libraries expose types, applications consume them —
thiserrorvsanyhow - Errors are data — pattern match, convert, enrich, log
Further reading: Rust Error Handling in 2026 | thiserror docs | anyhow docs