1006 — Multiple Error Types
Tutorial
The Problem
Handle functions that return different error types in the same call chain. Compare two approaches: Box<dyn Error> (flexible, type-erased) and a typed AppError enum (exhaustive, structured). Implement From conversions for the enum approach to enable ? operator chaining. Compare with OCaml's unified variant and polymorphic variants.
🎯 Learning Outcomes
Box<dyn std::error::Error> as a universal error type that accepts any Error implementor? on ParseIntError in a -> Result<T, Box<dyn Error>> function auto-boxes via FromAppError enum with From impls for each sub-error typeBox<dyn Error> (flexible/simple) vs enum (exhaustive/structured)Box<dyn Error> for applications, typed enum for librariesCode Example
#![allow(clippy::all)]
// 1006: Multiple Error Types
// Unifying multiple error types: Box<dyn Error> vs enum approach
use std::fmt;
use std::num::ParseIntError;
// Individual error types
#[derive(Debug)]
struct IoError(String);
impl fmt::Display for IoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "IO error: {}", self.0)
}
}
impl std::error::Error for IoError {}
#[derive(Debug)]
struct NetError(String);
impl fmt::Display for NetError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "network error: {}", self.0)
}
}
impl std::error::Error for NetError {}
// Approach 1: Box<dyn Error> — quick and flexible
fn do_io_boxed() -> Result<String, Box<dyn std::error::Error>> {
Err(Box::new(IoError("file not found".into())))
}
fn do_parse_boxed(s: &str) -> Result<i64, Box<dyn std::error::Error>> {
let n: i64 = s.parse()?; // ParseIntError auto-boxed
Ok(n)
}
fn do_net_boxed() -> Result<String, Box<dyn std::error::Error>> {
Err(Box::new(NetError("timeout".into())))
}
fn process_boxed() -> Result<i64, Box<dyn std::error::Error>> {
let data = do_io_boxed().or_else(|_| Ok::<_, Box<dyn std::error::Error>>("42".into()))?;
let parsed = do_parse_boxed(&data)?;
let _response =
do_net_boxed().or_else(|_| Ok::<String, Box<dyn std::error::Error>>("ok".into()))?;
Ok(parsed)
}
// Approach 2: Typed enum — exhaustive matching
#[derive(Debug)]
enum AppError {
Io(IoError),
Parse(ParseIntError),
Net(NetError),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "{}", e),
AppError::Parse(e) => write!(f, "parse: {}", e),
AppError::Net(e) => write!(f, "{}", e),
}
}
}
impl std::error::Error for AppError {}
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)
}
}
impl From<NetError> for AppError {
fn from(e: NetError) -> Self {
AppError::Net(e)
}
}
fn do_io_typed() -> Result<String, IoError> {
Ok("42".into())
}
fn do_parse_typed(s: &str) -> Result<i64, ParseIntError> {
s.parse()
}
fn process_typed() -> Result<i64, AppError> {
let data = do_io_typed()?; // IoError -> AppError
let parsed = do_parse_typed(&data)?; // ParseIntError -> AppError
Ok(parsed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_boxed_error() {
let result = process_boxed();
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_boxed_io_error() {
let err = do_io_boxed().unwrap_err();
assert!(err.to_string().contains("IO error"));
}
#[test]
fn test_typed_success() {
assert_eq!(process_typed().unwrap(), 42);
}
#[test]
fn test_typed_pattern_match() {
let err: AppError = IoError("test".into()).into();
assert!(matches!(err, AppError::Io(_)));
let err: AppError = "abc".parse::<i64>().unwrap_err().into();
assert!(matches!(err, AppError::Parse(_)));
}
#[test]
fn test_boxed_parse_error() {
let result = do_parse_boxed("not_a_number");
assert!(result.is_err());
}
#[test]
fn test_display_format() {
let err = AppError::Net(NetError("timeout".into()));
assert_eq!(err.to_string(), "network error: timeout");
}
}Key Differences
| Aspect | Rust Box<dyn Error> | Rust typed enum | OCaml unified variant | OCaml poly variants |
|---|---|---|---|---|
| Exhaustiveness | No | Yes | Yes | No |
| Conversion | Auto (blanket From) | Explicit From impls | Manual wrapping | Structural subtyping |
| Pattern match | Downcast needed | Direct match | Direct match | Flexible |
| Library use | Not recommended | Recommended | Recommended | Possible |
| Verbosity | Low | Medium | Low | Low |
The general rule: use typed enums for library crates (callers need to match on errors), use Box<dyn Error> or anyhow::Error for application code where errors are logged rather than matched.
OCaml Approach
OCaml's standard approach is a unified app_error variant: type app_error = Io of io_error | Parse of parse_error | Net of net_error. Functions explicitly wrap errors: Result.map_error (fun e -> Io e). Polymorphic variants ([> \FileNotFound \| \ReadError of string]) provide open, extensible error types without a central enum — more flexible but harder to reason about exhaustively.
Full Source
#![allow(clippy::all)]
// 1006: Multiple Error Types
// Unifying multiple error types: Box<dyn Error> vs enum approach
use std::fmt;
use std::num::ParseIntError;
// Individual error types
#[derive(Debug)]
struct IoError(String);
impl fmt::Display for IoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "IO error: {}", self.0)
}
}
impl std::error::Error for IoError {}
#[derive(Debug)]
struct NetError(String);
impl fmt::Display for NetError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "network error: {}", self.0)
}
}
impl std::error::Error for NetError {}
// Approach 1: Box<dyn Error> — quick and flexible
fn do_io_boxed() -> Result<String, Box<dyn std::error::Error>> {
Err(Box::new(IoError("file not found".into())))
}
fn do_parse_boxed(s: &str) -> Result<i64, Box<dyn std::error::Error>> {
let n: i64 = s.parse()?; // ParseIntError auto-boxed
Ok(n)
}
fn do_net_boxed() -> Result<String, Box<dyn std::error::Error>> {
Err(Box::new(NetError("timeout".into())))
}
fn process_boxed() -> Result<i64, Box<dyn std::error::Error>> {
let data = do_io_boxed().or_else(|_| Ok::<_, Box<dyn std::error::Error>>("42".into()))?;
let parsed = do_parse_boxed(&data)?;
let _response =
do_net_boxed().or_else(|_| Ok::<String, Box<dyn std::error::Error>>("ok".into()))?;
Ok(parsed)
}
// Approach 2: Typed enum — exhaustive matching
#[derive(Debug)]
enum AppError {
Io(IoError),
Parse(ParseIntError),
Net(NetError),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "{}", e),
AppError::Parse(e) => write!(f, "parse: {}", e),
AppError::Net(e) => write!(f, "{}", e),
}
}
}
impl std::error::Error for AppError {}
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)
}
}
impl From<NetError> for AppError {
fn from(e: NetError) -> Self {
AppError::Net(e)
}
}
fn do_io_typed() -> Result<String, IoError> {
Ok("42".into())
}
fn do_parse_typed(s: &str) -> Result<i64, ParseIntError> {
s.parse()
}
fn process_typed() -> Result<i64, AppError> {
let data = do_io_typed()?; // IoError -> AppError
let parsed = do_parse_typed(&data)?; // ParseIntError -> AppError
Ok(parsed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_boxed_error() {
let result = process_boxed();
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_boxed_io_error() {
let err = do_io_boxed().unwrap_err();
assert!(err.to_string().contains("IO error"));
}
#[test]
fn test_typed_success() {
assert_eq!(process_typed().unwrap(), 42);
}
#[test]
fn test_typed_pattern_match() {
let err: AppError = IoError("test".into()).into();
assert!(matches!(err, AppError::Io(_)));
let err: AppError = "abc".parse::<i64>().unwrap_err().into();
assert!(matches!(err, AppError::Parse(_)));
}
#[test]
fn test_boxed_parse_error() {
let result = do_parse_boxed("not_a_number");
assert!(result.is_err());
}
#[test]
fn test_display_format() {
let err = AppError::Net(NetError("timeout".into()));
assert_eq!(err.to_string(), "network error: timeout");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_boxed_error() {
let result = process_boxed();
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_boxed_io_error() {
let err = do_io_boxed().unwrap_err();
assert!(err.to_string().contains("IO error"));
}
#[test]
fn test_typed_success() {
assert_eq!(process_typed().unwrap(), 42);
}
#[test]
fn test_typed_pattern_match() {
let err: AppError = IoError("test".into()).into();
assert!(matches!(err, AppError::Io(_)));
let err: AppError = "abc".parse::<i64>().unwrap_err().into();
assert!(matches!(err, AppError::Parse(_)));
}
#[test]
fn test_boxed_parse_error() {
let result = do_parse_boxed("not_a_number");
assert!(result.is_err());
}
#[test]
fn test_display_format() {
let err = AppError::Net(NetError("timeout".into()));
assert_eq!(err.to_string(), "network error: timeout");
}
}
Deep Comparison
Multiple Error Types — Comparison
Core Insight
When functions call code with different error types, you need a unification strategy. Rust offers two: type-erased (Box<dyn Error>) and typed (enum with From impls).
OCaml Approach
Box<dyn Error>Rust Approach
Box<dyn Error>: any error type auto-converts, but you lose pattern matchingFrom impls: more boilerplate, full pattern matching retained? operator works with both approachesComparison Table
| Aspect | OCaml Variant | OCaml Poly Variant | Rust Box<dyn> | Rust Enum |
|---|---|---|---|---|
| Setup cost | Medium | Low | Low | Medium |
| Pattern matching | Yes | Partial | No (need downcast) | Yes, exhaustive |
| Extensibility | Closed | Open | Open | Closed |
| Performance | Zero-cost | Zero-cost | Heap allocation | Zero-cost |
| Best for | Libraries | Prototyping | Scripts/prototypes | Libraries/apps |
Exercises
anyhow::anyhow!("message") and anyhow::Context::context to rewrite process_boxed without defining any error types.AppError::Config(String) variant and implement its From conversion.fn collect_errors(results: Vec<Result<i32, AppError>>) -> (Vec<i32>, Vec<AppError>) that separates successes and errors.fmt::Display for AppError using .source() to print the full chain.app_error with a WithContext { context: string; inner: app_error } variant and write a display_chain function that prints the full error hierarchy.