Skip to content

Fix: Rust Error Handling Not Working — ? Operator, Custom Error Types, and thiserror/anyhow

FixDevs ·

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 From implementation? needs From<io::Error> for AppError and From<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 like anyhow.
  • ? in main() requires special handlingmain() can return Result<(), E> only if E: Debug. Use Box<dyn Error> or anyhow::Error for 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:

thiserroranyhow
Best forLibrariesApplications
Error matchingYes — enum variantsNo — opaque type
Error messagesDefined at type levelAttached at use site
OverheadZeroSmall allocation
Caller handles specific errorsYesNo

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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles