1004 — Error Conversion
Tutorial
The Problem
Implement From<SubError> for AppError to enable automatic error conversion with the ? operator. When a function returns Result<T, AppError> and calls a sub-function returning Result<T, ParseIntError>, the ? operator automatically wraps the inner error via From::from. Compare with OCaml's explicit manual wrapping.
🎯 Learning Outcomes
impl From<IoError> for AppError and impl From<ParseIntError> for AppError? desugars to map_err(From::from) — calling the From implError::source to expose the wrapped error for error chain inspection? calls in a single function without explicit map_errFrom-based conversion to OCaml's manual IoError(e) wrappingAppError unified error enum pattern as the idiomatic Rust designCode Example
#![allow(clippy::all)]
// 1004: Error Conversion
// From trait for automatic error conversion with ? operator
use std::fmt;
use std::num::ParseIntError;
// Sub-error types
#[derive(Debug)]
enum IoError {
FileNotFound(String),
PermissionDenied(String),
}
impl fmt::Display for IoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
IoError::FileNotFound(p) => write!(f, "file not found: {}", p),
IoError::PermissionDenied(p) => write!(f, "permission denied: {}", p),
}
}
}
impl std::error::Error for IoError {}
// Unified app error with From impls
#[derive(Debug)]
enum AppError {
Io(IoError),
Parse(ParseIntError),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO: {}", e),
AppError::Parse(e) => write!(f, "Parse: {}", e),
}
}
}
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
AppError::Io(e) => Some(e),
AppError::Parse(e) => Some(e),
}
}
}
// From impls enable automatic conversion with ?
impl From<IoError> for AppError {
fn from(e: IoError) -> Self {
AppError::Io(e)
}
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::Parse(e)
}
}
// Functions that return sub-errors
fn read_config(path: &str) -> Result<String, IoError> {
if path == "/missing" {
Err(IoError::FileNotFound(path.to_string()))
} else {
Ok("42".to_string())
}
}
// The ? operator automatically calls From to convert errors
fn load_config(path: &str) -> Result<i64, AppError> {
let content = read_config(path)?; // IoError -> AppError via From
let value: i64 = content.parse()?; // ParseIntError -> AppError via From
Ok(value)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_successful_load() {
assert_eq!(load_config("/ok").unwrap(), 42);
}
#[test]
fn test_io_error_conversion() {
let result = load_config("/missing");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, AppError::Io(IoError::FileNotFound(_))));
assert!(err.to_string().contains("file not found"));
}
#[test]
fn test_from_io_error() {
let io_err = IoError::FileNotFound("test".into());
let app_err: AppError = io_err.into();
assert!(matches!(app_err, AppError::Io(_)));
}
#[test]
fn test_from_parse_error() {
let parse_err: ParseIntError = "abc".parse::<i64>().unwrap_err();
let app_err: AppError = parse_err.into();
assert!(matches!(app_err, AppError::Parse(_)));
}
#[test]
fn test_error_source_chain() {
use std::error::Error;
let result = load_config("/missing");
let err = result.unwrap_err();
// source() returns the inner error
assert!(err.source().is_some());
}
#[test]
fn test_question_mark_converts() {
fn inner() -> Result<i64, AppError> {
let _s = read_config("/ok")?; // auto-converts IoError
Ok(42)
}
assert_eq!(inner().unwrap(), 42);
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Auto-conversion | From impl + ? | Manual wrapping IoError(e) |
? operator | map_err(From::from) + early return | let* bind + manual error lifting |
| Error chain | Error::source() | No standard protocol |
| Wrapper enum | AppError::Io(e), AppError::Parse(e) | Same variant wrapping |
| From boilerplate | 5-line impl per error type | Manual fun e -> IoError e at each call |
| thiserror | #[from] attribute eliminates From | No equivalent |
The From + ? pattern is one of Rust's most important ergonomic features. Writing some_fallible_call()? in a function returning Result<T, AppError> automatically converts any matching sub-error type. This enables clean, readable error propagation without noise.
OCaml Approach
OCaml wraps errors manually: Error (IoError (FileNotFound path)). There is no ?-equivalent or automatic conversion. Functions returning app_error Result must explicitly tag sub-errors: Result.map_error (fun e -> IoError e) (read_file path). OCaml 4.08+ provides let* (monadic bind) for result chaining, but conversion is still explicit.
Full Source
#![allow(clippy::all)]
// 1004: Error Conversion
// From trait for automatic error conversion with ? operator
use std::fmt;
use std::num::ParseIntError;
// Sub-error types
#[derive(Debug)]
enum IoError {
FileNotFound(String),
PermissionDenied(String),
}
impl fmt::Display for IoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
IoError::FileNotFound(p) => write!(f, "file not found: {}", p),
IoError::PermissionDenied(p) => write!(f, "permission denied: {}", p),
}
}
}
impl std::error::Error for IoError {}
// Unified app error with From impls
#[derive(Debug)]
enum AppError {
Io(IoError),
Parse(ParseIntError),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO: {}", e),
AppError::Parse(e) => write!(f, "Parse: {}", e),
}
}
}
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
AppError::Io(e) => Some(e),
AppError::Parse(e) => Some(e),
}
}
}
// From impls enable automatic conversion with ?
impl From<IoError> for AppError {
fn from(e: IoError) -> Self {
AppError::Io(e)
}
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::Parse(e)
}
}
// Functions that return sub-errors
fn read_config(path: &str) -> Result<String, IoError> {
if path == "/missing" {
Err(IoError::FileNotFound(path.to_string()))
} else {
Ok("42".to_string())
}
}
// The ? operator automatically calls From to convert errors
fn load_config(path: &str) -> Result<i64, AppError> {
let content = read_config(path)?; // IoError -> AppError via From
let value: i64 = content.parse()?; // ParseIntError -> AppError via From
Ok(value)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_successful_load() {
assert_eq!(load_config("/ok").unwrap(), 42);
}
#[test]
fn test_io_error_conversion() {
let result = load_config("/missing");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, AppError::Io(IoError::FileNotFound(_))));
assert!(err.to_string().contains("file not found"));
}
#[test]
fn test_from_io_error() {
let io_err = IoError::FileNotFound("test".into());
let app_err: AppError = io_err.into();
assert!(matches!(app_err, AppError::Io(_)));
}
#[test]
fn test_from_parse_error() {
let parse_err: ParseIntError = "abc".parse::<i64>().unwrap_err();
let app_err: AppError = parse_err.into();
assert!(matches!(app_err, AppError::Parse(_)));
}
#[test]
fn test_error_source_chain() {
use std::error::Error;
let result = load_config("/missing");
let err = result.unwrap_err();
// source() returns the inner error
assert!(err.source().is_some());
}
#[test]
fn test_question_mark_converts() {
fn inner() -> Result<i64, AppError> {
let _s = read_config("/ok")?; // auto-converts IoError
Ok(42)
}
assert_eq!(inner().unwrap(), 42);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_successful_load() {
assert_eq!(load_config("/ok").unwrap(), 42);
}
#[test]
fn test_io_error_conversion() {
let result = load_config("/missing");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, AppError::Io(IoError::FileNotFound(_))));
assert!(err.to_string().contains("file not found"));
}
#[test]
fn test_from_io_error() {
let io_err = IoError::FileNotFound("test".into());
let app_err: AppError = io_err.into();
assert!(matches!(app_err, AppError::Io(_)));
}
#[test]
fn test_from_parse_error() {
let parse_err: ParseIntError = "abc".parse::<i64>().unwrap_err();
let app_err: AppError = parse_err.into();
assert!(matches!(app_err, AppError::Parse(_)));
}
#[test]
fn test_error_source_chain() {
use std::error::Error;
let result = load_config("/missing");
let err = result.unwrap_err();
// source() returns the inner error
assert!(err.source().is_some());
}
#[test]
fn test_question_mark_converts() {
fn inner() -> Result<i64, AppError> {
let _s = read_config("/ok")?; // auto-converts IoError
Ok(42)
}
assert_eq!(inner().unwrap(), 42);
}
}
Deep Comparison
Error Conversion — Comparison
Core Insight
Rust's From trait + ? operator automates what OCaml forces you to do manually: wrapping sub-errors into a unified error type.
OCaml Approach
Error (IoError e) at every call sitelift_* helper functions but they're boilerplateRust Approach
From<SubError> for UnifiedError once per sub-error type? operator automatically calls .into() which uses FromFrom impl, zero call-site changessource() method preserves the error chainComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Conversion mechanism | Manual wrapping | From trait + ? |
| Boilerplate per call site | One wrapper per call | Zero (automatic) |
| Adding new error source | Touch every call site | One From impl |
| Error chain | Manual nesting | source() method |
| Type safety | Variant pattern match | Same + compiler enforced |
Exercises
DbError(String) to AppError with a From<DbError> for AppError impl.fn process_all(items: Vec<&str>) -> Result<Vec<i32>, AppError> that parses all items, collecting the first error.Result::map_err manually to convert an IoError to AppError without the From impl, and compare verbosity.impl std::error::Error::source chaining for three levels: AppError → IoError → std::io::Error.map_error : ('a -> 'b) -> ('c, 'a) result -> ('c, 'b) result and use it to build a clean error-lifting pipeline.