Rust Error Conversion With From
How Rust's From trait powers the question-mark operator and lets you compose error types cleanly across libraries, applications, and async code.
What you'll learn
- ✓Core concept introduced
- ✓How the API is structured
- ✓Typical idiomatic usage
- ✓Common pitfalls to avoid
- ✓When and where to apply it
Prerequisites
- •Basic Rust familiarity
What and Why
In Rust, error handling is explicit. Functions return Result<T, E>, and the ? operator propagates errors up the stack. The magic that makes ? ergonomic is the From trait. When you write let x = some_call()?;, the compiler effectively rewrites it as let x = match some_call() { Ok(v) => v, Err(e) => return Err(From::from(e)) };. That From::from call is where error conversion happens.
The “why” is composition. Real applications integrate dozens of libraries, each with its own error type. Without conversions, you’d manually map_err at every call site. With From impls, the ? operator silently bridges error types, keeping happy-path code clean.
Mental Model
Picture each error type as a node in a graph. An edge from A to B exists if you’ve implemented impl From<A> for B. The ? operator can traverse a single edge per use, converting the inner error to the outer function’s error type.
Two patterns dominate. Application errors are usually a single enum (AppError) with one variant per upstream error source, plus From impls that wrap each one. Library errors are usually narrower - they expose a small, stable error enum so callers can pattern match on meaningful cases without depending on transitive sources.
Hands-on Example
Here is a typical application-level error enum.
use std::io;
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(std::num::ParseIntError),
NotFound,
}
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self { AppError::Io(e) }
}
impl From<std::num::ParseIntError> for AppError {
fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}
fn load_count(path: &str) -> Result<i32, AppError> {
let s = std::fs::read_to_string(path)?; // io::Error -> AppError
let n: i32 = s.trim().parse()?; // ParseIntError -> AppError
Ok(n)
}
Both ? calls succeed because AppError knows how to consume each upstream error.
The diagram shows two upstream error types funneling through From conversions into one unified AppError.
Common Pitfalls
The most common pitfall is implementing From for an error you don’t own and a target you don’t own - the orphan rule will reject it. The workaround is to wrap in your own newtype, then implement From on that newtype.
Another mistake is making From conversions lossy. If you flatten three upstream errors into one Other(String) variant, you lose the ability to match on the cause later. Prefer distinct variants per upstream type.
Watch out for accidental ? ambiguity in main. If your main returns Result<(), Box<dyn Error>>, almost everything converts via From<E> for Box<dyn Error>, which is convenient but hides structure. In real apps, define an AppError enum and convert explicitly.
Finally, From impls are infallible. If conversion can fail (e.g., extracting a field), use TryFrom instead and call map_err or use a fallible adapter.
Practical Tips
For applications, reach for thiserror. It generates From impls and Display implementations from a single derive:
#[derive(thiserror::Error, Debug)]
enum AppError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("parse error: {0}")]
Parse(#[from] std::num::ParseIntError),
}
For library boundaries, expose a stable enum and avoid leaking dependency error types into your public API. For binaries and prototypes, anyhow::Error is a great catch-all that works with ? everywhere and preserves backtraces.
When error conversion needs extra context, use .map_err or anyhow::Context::with_context instead of forcing a From impl that loses information.
Wrap-up
From is the quiet hero of Rust error handling. It turns ? into a powerful, polymorphic propagation operator while keeping conversions explicit and type-safe. Learn the orphan rule, prefer distinct variants over lossy flattening, and lean on thiserror or anyhow depending on whether you’re writing a library or an app. Your error paths will feel as smooth as the happy ones.
Related articles
- Rust Rust Error Handling with Result
How idiomatic Rust handles errors with Result and the ? operator: propagation, conversion, custom error types, and when to use anyhow or thiserror.
- Rust Rust Actix vs Axum Comparison
A side-by-side comparison of Actix Web and Axum, covering architecture, ergonomics, performance, ecosystem, and how to pick the right one for your project.
- Rust Rust async/await and Futures Explained
How Rust's async/await desugars into a state machine, what a Future actually is, and the runtime model that makes it efficient on real workloads.
- Rust Rust Axum Web Framework Tutorial
A practical introduction to building HTTP services in Rust using the Axum web framework, with routing, extractors, state, and JSON handling.