Fix: Rust Error Handling Not Working — ? Operator, Custom Error Types, and thiserror/anyhow
Quick Answer
How to fix Rust error handling issues — the ? operator, From trait for error conversion, thiserror for custom errors, anyhow for applications, and Box<dyn Error> pitfalls.
The Problem
The ? operator fails to compile with a type mismatch:
use std::fs;
use std::num::ParseIntError;
fn read_number(path: &str) -> Result<i32, ParseIntError> {
let content = fs::read_to_string(path)?; // Error!
// the `?` operator can only be used in a function that returns `Result`
// or `Option`, but this function returns `Result<i32, ParseIntError>`
// cannot convert `std::io::Error` into `ParseIntError`
content.trim().parse()
}Or a custom error type doesn’t work with ?:
#[derive(Debug)]
enum AppError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
}
fn process() -> Result<i32, AppError> {
let content = fs::read_to_string("file.txt")?; // Error: io::Error ≠ AppError
let n: i32 = content.trim().parse()?; // Error: ParseIntError ≠ AppError
Ok(n)
}Or Box<dyn std::error::Error> compiles but loses error type information:
fn run() -> Result<(), Box<dyn std::error::Error>> {
let n: i32 = "not a number".parse()?; // Works, but how do you handle specific errors?
Ok(())
}Why This Happens
Rust’s ? operator desugars to a match that calls From::from(err) on the error. For ? to work, the error type being converted must implement From<SourceError> for the function’s return error type. Without that From impl, Rust can’t convert between error types.
- No
Fromimplementation —?needsFrom<io::Error> for AppErrorandFrom<ParseIntError> for AppError. Without both, the conversions fail at compile time. - Heterogeneous error types — a function that can fail with multiple different error types requires either a common enum, a trait object (
Box<dyn Error>), or a library likeanyhow. ?inmain()requires special handling —main()can returnResult<(), E>only ifE: Debug. UseBox<dyn Error>oranyhow::Errorfor convenience.
Fix 1: Implement From for Each Error Type
The ? operator calls From::from() to convert errors. Implement From for each source error:
use std::fs;
use std::num::ParseIntError;
use std::io;
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
}
// Implement From<io::Error> for AppError
impl From<io::Error> for AppError {
fn from(err: io::Error) -> Self {
AppError::Io(err)
}
}
// Implement From<ParseIntError> for AppError
impl From<ParseIntError> for AppError {
fn from(err: ParseIntError) -> Self {
AppError::Parse(err)
}
}
// Now ? works for both io::Error and ParseIntError
fn read_number(path: &str) -> Result<i32, AppError> {
let content = fs::read_to_string(path)?; // io::Error → AppError::Io
let n: i32 = content.trim().parse()?; // ParseIntError → AppError::Parse
Ok(n)
}
fn main() {
match read_number("numbers.txt") {
Ok(n) => println!("Number: {}", n),
Err(AppError::Io(e)) => eprintln!("IO error: {}", e),
Err(AppError::Parse(e)) => eprintln!("Parse error: {}", e),
}
}Also implement std::fmt::Display for the error type:
use std::fmt;
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO error: {}", e),
AppError::Parse(e) => write!(f, "Parse error: {}", e),
}
}
}
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
AppError::Io(e) => Some(e),
AppError::Parse(e) => Some(e),
}
}
}Fix 2: Use thiserror for Library Code
The thiserror crate generates Display, Error, and From implementations automatically:
# Cargo.toml
[dependencies]
thiserror = "1"use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("IO error: {0}")] // Display implementation
Io(#[from] std::io::Error), // From<io::Error> generated automatically
#[error("Parse error: {0}")]
Parse(#[from] std::num::ParseIntError),
#[error("User not found: id={id}")]
UserNotFound { id: u64 },
#[error("Invalid config: {message}")]
Config { message: String },
#[error("Database error")]
Database(#[source] sqlx::Error), // #[source] without #[from]: won't auto-convert
}
// Now ? works directly — thiserror generated From impls
fn read_number(path: &str) -> Result<i32, AppError> {
let content = std::fs::read_to_string(path)?; // io::Error → AppError::Io
let n: i32 = content.trim().parse()?; // ParseIntError → AppError::Parse
Ok(n)
}#[from] vs #[source]:
#[derive(Debug, Error)]
enum AppError {
// #[from] — generates From<sqlx::Error> AND sets the source
#[error("DB error: {0}")]
Database(#[from] sqlx::Error),
// #[source] — only sets the error source (for error chains)
// Does NOT generate From — you must convert manually
#[error("Cache error")]
Cache(#[source] redis::RedisError),
}Fix 3: Use anyhow for Application Code
anyhow is ideal for application-level code where you don’t need to match on specific error types:
[dependencies]
anyhow = "1"use anyhow::{Context, Result, bail, ensure};
// anyhow::Result<T> is Result<T, anyhow::Error>
fn process_file(path: &str) -> Result<i32> {
// Any error type that implements std::error::Error works with ?
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read file: {}", path))?;
let n: i32 = content.trim().parse()
.context("Failed to parse file content as integer")?;
Ok(n)
}
// bail! — return an error immediately
fn validate_age(age: i32) -> Result<()> {
if age < 0 {
bail!("Age cannot be negative: {}", age);
}
if age > 150 {
bail!("Age seems unrealistic: {}", age);
}
Ok(())
}
// ensure! — like assert! but returns an error instead of panicking
fn check_permissions(user_id: u64, required_role: &str) -> Result<()> {
let user = get_user(user_id)?;
ensure!(
user.roles.contains(&required_role.to_string()),
"User {} lacks required role: {}",
user_id,
required_role
);
Ok(())
}
fn main() -> Result<()> {
let n = process_file("input.txt")?;
println!("Number: {}", n);
Ok(())
}When to use thiserror vs anyhow:
thiserror | anyhow | |
|---|---|---|
| Best for | Libraries | Applications |
| Error matching | Yes — enum variants | No — opaque type |
| Error messages | Defined at type level | Attached at use site |
| Overhead | Zero | Small allocation |
| Caller handles specific errors | Yes | No |
Fix 4: Fix ? in Functions Returning Option
? also works in functions returning Option:
fn find_first_even(nums: &[i32]) -> Option<i32> {
// ? on Option returns None if the value is None
let first = nums.first()?; // Returns None if slice is empty
nums.iter().find(|&&n| n % 2 == 0).copied()
}
// Converting between Result and Option
fn parse_optional(s: Option<&str>) -> Result<i32, std::num::ParseIntError> {
// ok_or / ok_or_else converts Option to Result
let s = s.ok_or_else(|| "missing value".parse::<i32>().unwrap_err())?;
s.parse()
}
// Using ? with both Option and Result — requires matching return types
fn mixed() -> Option<i32> {
// ? on Result inside Option-returning function — doesn't compile directly
// Use .ok()? to convert Result<T, E> → Option<T>
let content = std::fs::read_to_string("file.txt").ok()?;
content.trim().parse::<i32>().ok()
}Fix 5: Error Handling Patterns in Async Code
? works in async functions the same as sync:
use tokio;
use anyhow::Result;
async fn fetch_user(id: u64) -> Result<User> {
let response = reqwest::get(format!("https://api.example.com/users/{}", id))
.await
.context("Failed to send request")?;
if !response.status().is_success() {
anyhow::bail!("API returned status {}", response.status());
}
let user: User = response.json()
.await
.context("Failed to parse response")?;
Ok(user)
}
// Handling multiple concurrent errors
async fn fetch_all(ids: Vec<u64>) -> Result<Vec<User>> {
let futures: Vec<_> = ids.iter().map(|&id| fetch_user(id)).collect();
let results = futures::future::join_all(futures).await;
// Collect results, fail on first error
results.into_iter().collect::<Result<Vec<_>>>()
}Fix 6: Map and Recover from Errors
Transform and recover from errors without early return:
use std::fs;
fn read_config(path: &str) -> String {
// Provide a default on error
fs::read_to_string(path).unwrap_or_else(|_| String::from("{}"))
}
fn parse_port(s: &str) -> u16 {
s.parse()
.map_err(|e| eprintln!("Invalid port '{}': {}", s, e))
.unwrap_or(8080) // Default port on parse error
}
fn chain_operations(path: &str) -> Result<i32, AppError> {
fs::read_to_string(path)
.map_err(AppError::Io)? // Convert io::Error to AppError::Io
.trim()
.parse::<i32>()
.map_err(AppError::Parse) // Convert ParseIntError to AppError::Parse
}
// map vs and_then
fn double_file_number(path: &str) -> Result<i32, AppError> {
read_number(path)
.map(|n| n * 2) // Transform Ok value
.and_then(|n| { // Chain another fallible operation
if n > 1000 {
Err(AppError::Config { message: "Number too large".to_string() })
} else {
Ok(n)
}
})
}Still Not Working?
? requires a return type of Result or Option — ? doesn’t work in a function that returns () or any other type. If you’re in main(), change the return type to Result<(), Box<dyn std::error::Error>> or anyhow::Result<()>.
Lifetime issues with Box<dyn Error> — Box<dyn std::error::Error> requires the error to be 'static by default. If your error contains references, use Box<dyn std::error::Error + 'lifetime>. This is rarely needed — prefer owned error types.
anyhow::Error in library crates — using anyhow::Error as a public return type forces library users to depend on anyhow. Use thiserror for public APIs, anyhow internally if needed.
Multiple ? operators with conflicting types — if a function uses ? on both io::Error and reqwest::Error, you need either a common error type with From impls for both, or use anyhow::Result which accepts any std::error::Error.
For related Rust issues, see Fix: Rust Borrow Checker Error and Fix: Rust Lifetime Error.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Go Test Not Working — Tests Not Running, Failing Unexpectedly, or Coverage Not Collected
How to fix Go testing issues — test function naming, table-driven tests, t.Run subtests, httptest, testify assertions, and common go test flag errors.
Fix: Java Record Not Working — Compact Constructor Error, Serialization Fails, or Cannot Extend
How to fix Java record issues — compact constructor validation, custom accessor methods, Jackson serialization, inheritance restrictions, and when to use records vs regular classes.
Fix: Kotlin Sealed Class Not Working — when Expression Not Exhaustive or Subclass Not Found
How to fix Kotlin sealed class issues — when exhaustiveness, sealed interface vs class, subclass visibility, Result pattern, and sealed classes across modules.
Fix: Python contextmanager Not Working — GeneratorExit, Missing yield, or Cleanup Not Running
How to fix Python context manager issues — @contextmanager generator, __enter__ and __exit__, exception handling inside with blocks, async context managers, and common pitfalls.