How to Implement From for Custom Error Conversion

Implement the From trait for your custom error enum to enable automatic conversion from standard library errors using the ? operator.

The bridge for the ? operator

You write a function that reads a configuration file and parses a port number. You define a custom AppError enum so your callers don't have to handle raw standard library errors. You slap a ? on the file read. The compiler rejects you with E0277. It says the trait From<std::io::Error> is not implemented for AppError. You're forced to write .map_err(|e| AppError::Io(e)) on every line. The logic gets buried in boilerplate. Error handling turns into noise.

The From trait fixes this. It tells the compiler how to convert foreign errors into your custom type. Once the conversion exists, ? uses it automatically. You get clean code without sacrificing type safety.

How From connects to ?

The ? operator is syntactic sugar. It doesn't just unwrap values. It also performs error conversion. When you write expr?, the compiler expands it behind the scenes. If expr succeeds, you get the value. If expr fails, the compiler looks for a way to convert the error into the error type of the current function.

That way is the From trait. The compiler searches for an implementation of From<SourceError> for TargetError. If it finds one, it calls From::from(error) to reshape the error and returns early. If it doesn't find one, compilation fails with E0277.

Think of From as a universal adapter. You have a socket that accepts your custom error type. The standard library hands you a plug shaped like io::Error. The socket rejects it. From is the adapter you build that snaps onto the io::Error plug and reshapes it so it fits your socket. Once the adapter exists, you can just plug things in. The ? operator looks for this adapter behind the scenes.

Minimal working example

Here is the pattern. You define an error enum. You implement From for each variant that wraps a foreign error. Then ? works everywhere.

use std::fmt;

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

// Display is required for std::error::Error convention.
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}"),
        }
    }
}

// Error trait marks this as a proper error type.
impl std::error::Error for AppError {}

// Bridge std::io::Error into AppError.
// This allows ? to convert io::Error to AppError::Io.
impl From<std::io::Error> for AppError {
    fn from(err: std::io::Error) -> Self {
        AppError::Io(err)
    }
}

// Bridge ParseIntError into AppError.
// This allows ? to convert ParseIntError to AppError::Parse.
impl From<std::num::ParseIntError> for AppError {
    fn from(err: std::num::ParseIntError) -> Self {
        AppError::Parse(err)
    }
}

fn read_port() -> Result<u16, AppError> {
    // ? uses From<std::io::Error> automatically.
    // No map_err needed.
    let content = std::fs::read_to_string("config.txt")?;

    // ? uses From<ParseIntError> automatically.
    let port: u16 = content.trim().parse()?;

    Ok(port)
}

Implement From, not Into. The community convention is to implement From<A> for B. The standard library automatically provides Into<B> for A as a free reverse implementation. If you implement Into, you miss out on the automatic From. Always write the From impl. It's the explicit direction that matters.

What happens at compile time

When the compiler sees std::fs::read_to_string("config.txt")? inside read_port, it performs a lookup. The function returns Result<String, std::io::Error>. The current function returns Result<u16, AppError>. The compiler checks if AppError implements From<std::io::Error>.

It finds the impl block. It generates code equivalent to:

match std::fs::read_to_string("config.txt") {
    Ok(val) => val,
    Err(e) => return Err(AppError::from(e)),
}

The conversion happens at the call site. The error is wrapped immediately. No runtime overhead beyond the function call. The compiler guarantees the conversion exists before the code runs. If you remove the From impl, the lookup fails and you get E0277. The error message points to the exact line and tells you which trait is missing.

The compiler checks the bridge before you cross. If the bridge is missing, the code doesn't compile.

Realistic error handling

In a real project, your error enum might have more variants. You might need to handle database errors, HTTP errors, or custom validation failures. The pattern scales. Each variant that wraps a foreign error gets a From impl.

use std::fmt;
use std::path::Path;

#[derive(Debug)]
struct Config {
    host: String,
    port: u16,
}

#[derive(Debug)]
enum ConfigError {
    FileRead(std::io::Error),
    ParsePort(std::num::ParseIntError),
    MissingField(String),
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ConfigError::FileRead(e) => write!(f, "Failed to read config: {e}"),
            ConfigError::ParsePort(e) => write!(f, "Invalid port number: {e}"),
            ConfigError::MissingField(field) => write!(f, "Missing field: {field}"),
        }
    }
}

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

impl From<std::io::Error> for ConfigError {
    fn from(err: std::io::Error) -> Self {
        ConfigError::FileRead(err)
    }
}

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

fn load_config(path: &Path) -> Result<Config, ConfigError> {
    // ? converts io::Error to ConfigError::FileRead.
    let content = std::fs::read_to_string(path)?;

    let mut lines = content.lines();
    let host = lines.next().unwrap_or("localhost").to_string();
    let port_str = lines.next().unwrap_or("8080");

    // ? converts ParseIntError to ConfigError::ParsePort.
    let port: u16 = port_str.parse()?;

    Ok(Config { host, port })
}

In production code, reach for thiserror. The thiserror crate generates these From impls automatically. You annotate your enum variants with #[from] and the derive macro writes the boilerplate. It keeps your error definitions readable and reduces copy-paste errors. The manual From impl is essential to understand, but the tooling handles the repetition.

Pitfalls and compiler errors

The most common mistake is getting the direction wrong. You want to convert io::Error into AppError. You write impl From<AppError> for std::io::Error. The compiler rejects this with E0117. It says you cannot implement a foreign trait for a foreign type.

The orphan rule requires that at least one type in the impl block is local to your crate. std::io::Error lives in the standard library. You can't add impls to it. You must put the impl on your error type. Write impl From<std::io::Error> for AppError. The target type must be local. The source can be anything.

Keep the local type on the right. impl From<Foreign> for Local compiles. The reverse hits the orphan rule wall.

Another trap is expecting From to add context. From::from takes only the source error. It cannot accept extra arguments. If you need to attach the filename to an IO error, From won't help. You have to use map_err.

// From cannot do this. It only takes the error.
// You need map_err to inject the path.
let content = std::fs::read_to_string(path)
    .map_err(|e| ConfigError::FileReadWithPath(e, path.to_path_buf()))?;

From is for simple wrapping. map_err is for enrichment. Know the difference.

When to use From vs alternatives

Use From when you need the ? operator to convert a specific error type into your custom error enum automatically.

Use thiserror with the #[from] attribute when your error enum has many variants and you want the compiler to generate the conversion code without writing manual impl blocks.

Use map_err when you need to attach extra context to the error during conversion, such as the path of a file that failed to read, because From takes only the error value and cannot accept additional arguments.

Reach for Into when you are passing an error to a function that accepts impl Into<SomeError> and you want to allow callers to provide convertible types, though you should still implement From to make the conversion happen.

Use TryFrom when the conversion can fail and returns a Result, but note that ? does not use TryFrom automatically; it only uses From.

Automate the conversion. Let ? do the heavy lifting so your error handling stays focused on recovery.

Where to go next