What is the From trait for error conversion

The From trait allows Rust to automatically convert one error type into another for seamless propagation.

The ? operator hits a wall

You are writing a function that reads a configuration file, parses JSON, and connects to a database. You want to return a single AppError type so the caller doesn't have to juggle three different error enums. You slap a ? on the file read. The compiler rejects you with E0277 (the trait bound AppError: From<std::io::Error> is not satisfied).

You have std::io::Error but you promised AppError. You could wrap every call in a map_err, turning your code into a wall of boilerplate. The From trait is the fix. It tells Rust how to convert one error into another automatically, so ? just works. You implement the conversion once. The compiler handles the rest.

From is the adapter

The From trait lives in std::convert. It defines a single method: fn from(source: Source) -> Self. It takes a source value and returns the target type. The conversion is infallible. It never returns a Result. If you need a conversion that can fail, that is TryFrom. For errors, conversion is usually safe. You can always wrap an error in a variant. You can always format a message. From is the bridge between error types.

Think of From as a universal adapter. You have a specific plug (the source error) and a specific socket (your target error). Without an adapter, you cannot connect them. From is that adapter. Once you plug it in, you can just push the plug in, and the electricity flows. In Rust terms, From<Source> for Target means "I know how to turn a Source into a Target." The ? operator looks for this adapter behind the scenes. If it finds one, it uses it. If not, you get a compile error.

Write the impl once. Let ? do the heavy lifting.

Minimal example

This example shows the bare minimum to make ? work across error types. You define a custom error, implement From for a standard error, and then use ? in a function.

/// A simple error type for our application.
#[derive(Debug)]
struct AppError(String);

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

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

/// Implement From to convert io::Error into AppError.
/// This allows the ? operator to work seamlessly.
impl From<std::io::Error> for AppError {
    fn from(err: std::io::Error) -> Self {
        // Wrap the IO error message in our custom error.
        AppError(format!("IO failure: {}", err))
    }
}

fn read_config() -> Result<String, AppError> {
    // The ? operator finds the From impl and converts automatically.
    std::fs::read_to_string("config.txt")?
}

The From implementation takes std::io::Error and returns AppError. The read_config function returns Result<String, AppError>. When read_to_string returns an error, the ? operator calls AppError::from(err). The conversion happens. The error propagates. Your function body stays clean.

Think of From as a one-way bridge. Traffic flows from source to target. You build the bridge. The compiler drives the cars.

How the compiler uses From

The ? operator is syntactic sugar. It expands to a match expression. If the value is Ok, it unwraps the value. If Err, it calls From::from on the error and returns early. The compiler inserts this call. It looks for From<Source> implemented for the return type's error.

Here is the desugaring of let content = std::fs::read_to_string(path)?; inside a function returning Result<String, AppError>:

let content = match std::fs::read_to_string(path) {
    Ok(val) => val,
    Err(err) => return Err(AppError::from(err)),
};

The compiler searches for impl From<std::io::Error> for AppError. It finds your implementation. It generates the call. This is why the direction matters. You implement From<Source> for Target. Not the other way around. The ? operator needs to convert the error it has into the error it needs to return.

The direction is fixed. Source goes to Target. Get the order wrong and the compiler will not guess.

Realistic error enum

Real applications use enums with variants. You don't just wrap strings. You preserve the structure. You also implement source() to chain errors. This allows debuggers and loggers to see the root cause.

/// Application errors with distinct variants.
#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(String),
    NotFound(String),
}

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO error: {}", e),
            AppError::Parse(e) => write!(f, "Parse error: {}", e),
            AppError::NotFound(e) => write!(f, "Not found: {}", e),
        }
    }
}

impl std::error::Error for AppError {
    /// Chain errors so root causes are visible.
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            AppError::Io(e) => Some(e),
            _ => None,
        }
    }
}

/// Convert io::Error to the Io variant.
impl From<std::io::Error> for AppError {
    fn from(err: std::io::Error) -> Self {
        AppError::Io(err)
    }
}

/// Convert serde_json errors to the Parse variant.
impl From<serde_json::Error> for AppError {
    fn from(err: serde_json::Error) -> Self {
        AppError::Parse(err.to_string())
    }
}

fn load_user(path: &str) -> Result<serde_json::Value, AppError> {
    let content = std::fs::read_to_string(path)?;
    // Both ? calls use From impls to convert to AppError.
    let value: serde_json::Value = serde_json::from_str(&content)?;
    Ok(value)
}

The source() method returns the underlying error if one exists. This creates a chain. Tools like tracing walk this chain to log the full context. Without source(), the root cause is hidden behind your wrapper.

The community convention for production code is to use the thiserror crate. It generates these From impls for you based on attributes. You write #[derive(thiserror::Error)] and #[from] on variants. It saves typing and reduces bugs. Hand-writing From is still valuable to understand the mechanics, but reach for thiserror when you ship.

Preserve the source. A wrapper without a chain is a dead end.

Pitfalls and compiler errors

The orphan rule

You cannot implement From for types you don't own. Rust enforces the orphan rule. You can only implement a trait if either the trait or the type is local to your crate. From is in std. std::io::Error is in std. You cannot implement From<MyError> for std::io::Error. The compiler rejects you with E0117 (cannot implement foreign trait for foreign type).

You must implement From<std::io::Error> for MyError. MyError is local. The impl is allowed. This rule prevents conflicts. If two crates could implement From for the same types, the compiler wouldn't know which one to use.

You can only implement traits where you own the type or the trait. Stick to your types.

From vs Into

You might see Into mentioned. Into is the reverse of From. If you implement From<A> for B, you automatically get Into<B> for A. The standard library provides a blanket implementation. You never need to implement Into. Always implement From. Implementing both is redundant and confusing.

Convention: Always implement From. Never implement Into. The compiler generates Into automatically.

Impl From. Let the compiler handle Into.

Losing context

If you convert an error by calling to_string(), you lose the error chain. The source() method returns None. Debugging becomes harder. Prefer wrapping the error in a variant. If you must format a message, consider keeping the original error available.

Don't swallow the source error. Preserve the chain so debugging doesn't become archaeology.

E0277

If you forget the From impl, you get E0277. The compiler says the trait bound AppError: From<std::io::Error> is not satisfied. This happens when you use ? on a function that returns a different error type. The fix is to implement From for the source error, or use map_err for a one-off conversion.

Trust the borrow checker. It usually has a point. The trait system is the same. If the compiler complains about a missing trait, the conversion path is missing. Add the impl.

Decision matrix

Use From when you need to convert one error type into another infallibly. Use From when you want the ? operator to work across different error types without manual map_err calls. Use TryFrom when the conversion itself might fail and needs to return a Result. Use map_err when you only need a one-off conversion and don't want to implement a trait. Use thiserror when you are writing a library or application and want to generate From impls automatically. Reach for Box<dyn Error> when you need to return a heterogeneous collection of errors and don't care about specific variants.

Where to go next