How to convert between error types

Convert between error types in Rust by implementing the From trait for your custom error enum and using the ? operator for automatic propagation.

The error type mismatch

You're writing a function that reads a configuration file and parses a number from it. Reading the file can fail with an std::io::Error. Parsing the string can fail with a std::num::ParseIntError. Your function signature needs to return a Result, but the Result type requires a single error type in the second slot. You can't return Result<i32, io::Error> because the parse might fail. You can't return Result<i32, ParseIntError> because the read might fail. You need a way to wrap both possibilities into one type so the caller can handle them together.

Rust solves this with the From trait. You define a custom error type that holds both possibilities, then implement From for each error you want to accept. Once that's done, the ? operator uses your implementation automatically to convert errors behind the scenes.

The universal adapter

Think of From like a universal power adapter. You have a device that only accepts a specific plug shape. You travel to countries with different plug shapes. You don't redesign the device for every country. You write adapters that convert each foreign plug into the shape your device expects.

In Rust, your custom error type is the device. The foreign error types are the plugs. The From implementation is the adapter. When you implement From<ForeignError> for MyError, you're telling the compiler: "I can turn a ForeignError into a MyError."

The ? operator is the mechanism that grabs the adapter. When you write operation()?, the compiler checks if the error type from operation can be converted into the error type of your function's return value. If you've implemented From, the compiler inserts the conversion call. If not, the code won't compile.

This design keeps conversions explicit. Rust doesn't guess how to turn an IO error into a parse error. You provide the rule. The compiler enforces it.

Minimal example

Define an enum with a variant for each error source. Implement From for each source. Use ? in the function body.

use std::io;

/// Application error type that wraps IO and parse errors.
#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Parse(std::num::ParseIntError),
}

/// Convert io::Error into AppError::Io.
impl From<io::Error> for AppError {
    fn from(err: io::Error) -> Self {
        // Wrap the IO error in our variant.
        AppError::Io(err)
    }
}

/// Convert ParseIntError into AppError::Parse.
impl From<std::num::ParseIntError> for AppError {
    fn from(err: std::num::ParseIntError) -> Self {
        // Wrap the parse error in our variant.
        AppError::Parse(err)
    }
}

/// Reads a line from stdin and parses it as an integer.
fn read_number() -> Result<i32, AppError> {
    // The ? operator checks for Err.
    // If Err, it calls AppError::from(err) and returns early.
    // If Ok, it unwraps the value.
    let mut input = String::new();
    io::stdin().read_line(&mut input)?;
    
    // Same pattern: ? converts ParseIntError to AppError.
    let value: i32 = input.trim().parse()?;
    Ok(value)
}

The ? operator is lazy. It only performs the conversion when an error actually occurs. If the read succeeds and the parse succeeds, zero conversion cost. The compiler generates code that unwraps the Ok value directly.

What happens under the hood

When you write let value = input.trim().parse()?;, the compiler expands this into a match expression. It looks at the return type of read_number, sees Result<i32, AppError>, and checks if AppError implements From<std::num::ParseIntError>.

If the implementation exists, the compiler generates code roughly equivalent to:

// Conceptual expansion of the ? operator.
match input.trim().parse() {
    Ok(val) => val,
    Err(e) => return Err(AppError::from(e)),
}

The AppError::from(e) call is the conversion. Because you implemented From, this call resolves to your code that wraps the error in AppError::Parse. The function returns Err(AppError::Parse(e)) immediately.

If you forget the From implementation, the compiler rejects the code with E0277. The error message says the trait bound is not satisfied. It tells you exactly which conversion is missing. You can't rely on implicit coercion. Every conversion must be declared.

Convention aside: Always implement From, never Into. The standard library provides a blanket implementation that gives you Into<Target> for free whenever you implement From<Self>. Implementing Into directly breaks this symmetry and confuses readers. The community expects From implementations.

Realistic example

In a real application, you often need to display errors to the user. Implement std::fmt::Display for your error enum to control the message. You can also add context during conversion if the raw error isn't enough.

use std::fmt;
use std::io;

#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Parse(std::num::ParseIntError),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            // Format IO errors with a user-friendly prefix.
            AppError::Io(e) => write!(f, "Failed to read input: {}", e),
            // Format parse errors with context about the expected type.
            AppError::Parse(e) => write!(f, "Expected an integer, got invalid input: {}", e),
        }
    }
}

impl From<io::Error> for AppError {
    fn from(err: io::Error) -> Self {
        AppError::Io(err)
    }
}

impl From<std::num::ParseIntError> for AppError {
    fn from(err: std::num::ParseIntError) -> Self {
        AppError::Parse(err)
    }
}

/// Reads a number from a file path and returns it doubled.
fn process_config(path: &str) -> Result<i32, AppError> {
    // ? converts io::Error to AppError automatically.
    let content = std::fs::read_to_string(path)?;
    
    // ? converts ParseIntError to AppError automatically.
    let number: i32 = content.trim().parse()?;
    Ok(number * 2)
}

fn main() {
    // Handle the error at the top level.
    match process_config("config.txt") {
        Ok(n) => println!("Result: {}", n),
        Err(e) => eprintln!("Error: {}", e),
    }
}

Don't leak implementation details. Wrap the error so the caller only sees what matters. The caller handles AppError, not io::Error or ParseIntError. This keeps your public API stable. You can change the internal error sources without breaking callers, as long as AppError stays the same.

Pitfalls and compiler errors

The most common error is E0277. This happens when you use ? but haven't implemented From for the error type. The compiler lists the missing trait implementation. Fix it by adding the impl From<SourceError> for TargetError block.

Another pitfall is the orphan rule. You cannot implement a foreign trait for a foreign type. This means you can't implement From<std::io::Error> for std::io::Error. Both types must be local to your crate for at least one of them. That's why you define AppError in your crate. AppError is local, so you can implement From for it even when the source error is from the standard library.

If you try to implement From for a type you don't own, the compiler rejects it. You must wrap the error in your own type. This rule prevents conflicting implementations across crates. It forces error handling to be explicit and localized.

Convention aside: The community splits error handling into two patterns. Libraries use thiserror to define precise error enums with minimal boilerplate. Applications use anyhow to stop converting errors and just handle them. If you're building a library, reach for thiserror. If you're writing a binary, anyhow saves you from defining enums for every function. The thiserror crate generates From implementations automatically based on derive attributes. It's the standard tool for library authors.

The compiler is strict about conversions for a reason. Implicit conversions hide bugs. From makes the conversion explicit in the type system. You see exactly where errors are wrapped and how they flow through the code.

When to use From versus alternatives

Use From when you control the target error type and want the ? operator to work seamlessly. Implement From for each error variant you want to wrap. This gives you zero-cost conversions and clean function bodies.

Use map_err when you need to transform an error but don't want to implement From, or when the conversion requires extra context not available in the From signature. map_err takes a closure, so you can capture variables from the surrounding scope. It's more verbose but more flexible for one-off conversions.

Use Box<dyn std::error::Error> when you're writing a quick script or a tool where defining a custom enum is overkill. This type can hold any error that implements the Error trait. It uses runtime dispatch, which adds a small performance cost and prevents the caller from matching on specific error variants. It's convenient but loses type safety.

Use thiserror when you have many error variants and want to avoid writing boilerplate From and Display implementations by hand. The derive macro generates the code for you. It follows the same semantics as manual implementations but with less typing. It's the community standard for library error types.

Pick the tool that matches your error surface. Small script? Box<dyn Error>. Library with precise errors? thiserror or manual From. Function that needs context? map_err.

Where to go next