296: From Trait for Error Conversion
Tutorial Video
Text description (accessibility)
This video demonstrates the "296: From Trait for Error Conversion" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Functions calling multiple libraries encounter multiple error types. Key difference from OCaml: 1. **Automatic conversion**: Rust's `?` calls `From::from()` at every error propagation point — zero boilerplate after the `impl From`; OCaml requires manual wrapping.
Tutorial
The Problem
Functions calling multiple libraries encounter multiple error types. Unifying them into a single application error type with match on every ? usage is boilerplate. The From<SourceError> trait enables automatic conversion: when impl From<ParseIntError> for AppError is defined, the ? operator automatically calls AppError::from(e) when propagating a ParseIntError. This is how Rust achieves zero-boilerplate error type unification.
🎯 Learning Outcomes
impl From<E> for AppError enables automatic ? conversion from EFrom for each error type a function might encounter? desugaring as Err(AppError::from(e))From to build layered error hierarchies without explicit mapping at each call siteCode Example
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}
fn process(s: &str) -> Result<i32, AppError> {
let n = s.parse::<i32>()?; // auto-converts via From
Ok(n)
}Key Differences
? calls From::from() at every error propagation point — zero boilerplate after the impl From; OCaml requires manual wrapping.From implementations create a directed conversion graph; adding a new library error requires one new impl From.From<E> impl per target type is allowed — prevents ambiguous conversions.Into derived**: impl From<A> for B automatically provides impl Into<B> for A — both directions are covered.OCaml Approach
OCaml does not have automatic error conversion. Each call site must explicitly wrap errors:
let process s =
match int_of_string_opt s with
| None -> Error (`Parse "not a number")
| Some n ->
if n < 0 then Error (`Logic "negative")
else Ok (n * 2)
Libraries like Error_monad (from Tezos) provide richer error composition, but there is no standard mechanism for automatic conversion.
Full Source
#![allow(clippy::all)]
//! # From Trait for Error Conversion
//!
//! `impl From<E> for MyErr` enables automatic error conversion via `?`.
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
pub enum AppError {
Parse(ParseIntError),
Logic(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Parse(e) => write!(f, "parse error: {}", e),
AppError::Logic(s) => write!(f, "logic error: {}", s),
}
}
}
// These From impls make `?` work seamlessly
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::Parse(e)
}
}
/// Parse a number - returns ParseIntError
pub fn parse_number(s: &str) -> Result<i32, ParseIntError> {
s.trim().parse()
}
/// Validate that a number is positive
pub fn validate_positive(n: i32) -> Result<i32, AppError> {
if n > 0 {
Ok(n)
} else {
Err(AppError::Logic(format!("{} is not positive", n)))
}
}
/// Process input - ? converts ParseIntError -> AppError automatically via From
pub fn process(input: &str) -> Result<i32, AppError> {
let n = parse_number(input)?; // From<ParseIntError> called
let validated = validate_positive(n)?;
Ok(validated * 2)
}
/// Alternative: explicit conversion with .into()
pub fn process_explicit(input: &str) -> Result<i32, AppError> {
let n = parse_number(input).map_err(AppError::from)?;
validate_positive(n).map(|v| v * 2)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_ok() {
assert_eq!(process("21").unwrap(), 42);
}
#[test]
fn test_process_parse_err() {
assert!(matches!(process("abc"), Err(AppError::Parse(_))));
}
#[test]
fn test_process_logic_err() {
assert!(matches!(process("-1"), Err(AppError::Logic(_))));
}
#[test]
fn test_from_conversion() {
let e: ParseIntError = "x".parse::<i32>().unwrap_err();
let app: AppError = e.into(); // via From
assert!(matches!(app, AppError::Parse(_)));
}
#[test]
fn test_process_whitespace() {
assert_eq!(process(" 42 ").unwrap(), 84);
}
#[test]
fn test_process_explicit_same() {
assert_eq!(process_explicit("21").unwrap(), 42);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_ok() {
assert_eq!(process("21").unwrap(), 42);
}
#[test]
fn test_process_parse_err() {
assert!(matches!(process("abc"), Err(AppError::Parse(_))));
}
#[test]
fn test_process_logic_err() {
assert!(matches!(process("-1"), Err(AppError::Logic(_))));
}
#[test]
fn test_from_conversion() {
let e: ParseIntError = "x".parse::<i32>().unwrap_err();
let app: AppError = e.into(); // via From
assert!(matches!(app, AppError::Parse(_)));
}
#[test]
fn test_process_whitespace() {
assert_eq!(process(" 42 ").unwrap(), 84);
}
#[test]
fn test_process_explicit_same() {
assert_eq!(process_explicit("21").unwrap(), 42);
}
}
Deep Comparison
OCaml vs Rust: From Trait for Errors
Pattern 1: Automatic Conversion
OCaml
(* Must manually convert at every call site *)
let process s =
let* n = parse_number s |> Result.map_error (fun e -> ParseErr e) in
let* v = validate n in
Ok v
Rust
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}
fn process(s: &str) -> Result<i32, AppError> {
let n = s.parse::<i32>()?; // auto-converts via From
Ok(n)
}
Pattern 2: Manual vs Implicit
OCaml
Result.map_error (fun e -> wrap e) result
Rust
result? // Calls From::from() automatically
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Conversion | Result.map_error at each site | impl From<E> once |
| Triggering | Explicit | Implicit via ? |
| Type inference | N/A | Compiler selects From impl |
| Boilerplate | Scattered | Centralized |
Exercises
AppError with four variants and implement From for each of: std::io::Error, std::num::ParseIntError, serde_json::Error (simulated), and String.From impl, the ? operator fails to compile, then add the impl and verify it compiles.AppError using ? without any explicit map_err.