How to Convert Between Error Types in Rust

Implement the From trait to define how Rust converts specific error types into your custom error enum for use with the ? operator.

The friction of mismatched error types

You write a function that reads a configuration file, parses a number, and sends it over a network. The file reading returns std::io::Error. The parsing returns std::num::ParseIntError. The network call returns a custom HttpError. Your function signature demands a single error type. You cannot return three different types from the same branch. You need a way to funnel them into one bucket without writing a dozen match statements.

How the conversion actually works

Rust solves this with the From trait. Think of it like an airport baggage claim system. Different airlines use different luggage tags. The carousel only accepts one standard tag format. Before your bag goes on the belt, a worker scans the airline tag, translates it into the standard format, and hands it to the carousel. Your function is the carousel. The From trait is the translation worker. It takes a specific error type, wraps it in your custom error, and hands it back so the ? operator can keep moving.

The ? operator does not magically convert types. It relies on a contract. When you place ? after an expression that returns Result<T, E1>, the compiler checks whether your function's return type Result<T, E2> satisfies E2: From<E1>. If the trait is implemented, the compiler inserts the conversion call automatically. If it is missing, compilation stops.

A minimal working example

Start with a custom error enum that wraps the external types you expect to encounter. Implement Display and std::error::Error so the type behaves like a proper Rust error. Then implement From for each external type you want to absorb.

use std::fmt;
use std::io;
use std::num::ParseIntError;

/// Custom error type that wraps external errors
#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Parse(ParseIntError),
}

// Implement Display so the error prints nicely
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO failure: {e}"),
            AppError::Parse(e) => write!(f, "Parse failure: {e}"),
        }
    }
}

// Mark it as a proper std::error::Error
impl std::error::Error for AppError {}

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

// Convert ParseIntError into AppError
impl From<ParseIntError> for AppError {
    fn from(err: ParseIntError) -> Self {
        AppError::Parse(err)
    }
}

/// Reads a file and parses the first line as an integer
fn read_and_parse(path: &str) -> Result<i32, AppError> {
    // The ? operator automatically calls From::from on errors
    let content = std::fs::read_to_string(path)?;
    let number: i32 = content.trim().parse()?;
    Ok(number)
}

What happens under the hood

When read_to_string fails, it returns Err(io::Error). The ? operator sees that the function expects Result<i32, AppError>. It checks the trait bounds. It finds impl From<io::Error> for AppError. The compiler desugars the ? into a match that calls AppError::from(err) on the error branch. The wrapped error returns early from the function. The same process repeats for parse. If both succeed, Ok(number) flows through unchanged.

The ? operator is just syntactic sugar for a conditional early return. It does not allocate. It does not panic. It simply routes the error through the From implementation you provided. The compiler guarantees that every error path is handled before the function exits.

Convention aside: the community almost always implements From rather than Into. The standard library provides a blanket implementation that gives you Into<T> for free whenever you implement From<T>. Implementing Into manually is redundant and breaks the convention. Stick to From.

Real-world error handling

Production code rarely stops at two error variants. You will encounter database drivers, HTTP clients, serialization libraries, and custom business logic failures. The pattern scales by adding variants and matching arms. You also gain the ability to attach context when the raw error is insufficient.

use std::fmt;
use std::io;
use std::num::ParseIntError;

/// Extended error type with context attachment
#[derive(Debug)]
enum ServiceError {
    Io(io::Error),
    Parse(ParseIntError),
    ConfigMissing(String),
}

// Implement Display with contextual messages
impl fmt::Display for ServiceError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ServiceError::Io(e) => write!(f, "File operation failed: {e}"),
            ServiceError::Parse(e) => write!(f, "Number parsing failed: {e}"),
            ServiceError::ConfigMissing(key) => write!(f, "Missing config key: {key}"),
        }
    }
}

impl std::error::Error for ServiceError {}

// Wrap IO errors automatically
impl From<io::Error> for ServiceError {
    fn from(err: io::Error) -> Self {
        ServiceError::Io(err)
    }
}

// Wrap parse errors automatically
impl From<ParseIntError> for ServiceError {
    fn from(err: ParseIntError) -> Self {
        ServiceError::Parse(err)
    }
}

/// Loads configuration and validates required fields
fn load_config(path: &str) -> Result<String, ServiceError> {
    // Read the file, converting IO errors via From
    let content = std::fs::read_to_string(path)?;
    
    // Check for a required marker string
    if !content.contains("VERSION=2") {
        // Return a custom error without relying on From
        return Err(ServiceError::ConfigMissing("VERSION".to_string()));
    }
    
    Ok(content)
}

Notice the manual Err(ServiceError::ConfigMissing(...)) return. The ? operator only triggers on Result or Option expressions. When you construct an error yourself, you return it directly. The compiler treats both paths identically once they share the same error type.

Common traps and compiler messages

You will hit friction when the trait bounds do not line up. The compiler will stop you with E0277 (trait bound not satisfied) if you try to use ? on an error type that lacks a From implementation. The message points directly to the missing trait. Add the implementation or use map_err to bridge the gap.

You will see E0308 (mismatched types) if you forget to wrap a value or if your function signature expects a different error type than what you are returning. The compiler shows the expected and found types side by side. Align the signature with the actual return path.

You cannot implement From for a type combination where neither the target nor the source is defined in your crate. This is the orphan rule. The compiler rejects it to prevent two crates from accidentally providing conflicting conversions for the same pair of types. If you need to convert between two external types, wrap one of them in a newtype or use a conversion function instead.

Convention aside: keep your From implementations focused on wrapping, not transforming. The purpose is to funnel errors into a single enum. If you need to mutate the error, attach context, or change its structure, use map_err inline. Mixing transformation logic into From makes the trait harder to reason about and breaks the expectation that ? is a transparent pass-through.

Choosing your conversion strategy

Use From when you control the target error type and want the ? operator to work automatically across multiple call sites. Use map_err when you need to transform an error inline without adding a trait implementation or when the conversion is specific to one function. Use Box<dyn std::error::Error> when you are writing a quick script or a top-level main function and do not want to define an enum. Use a crate like thiserror when your error enum grows beyond three variants and you want the compiler to generate the boilerplate for you.

Trust the trait system. The compiler will not let you leak an unhandled error type into a function that expects a different one. Write the conversion once, and let ? do the routing.

Where to go next