---
title: How to handle multiple error types
summary: Define a custom enum listing all error variants and implement the Error trait to handle them uniformly.
lang: en
slug: how-to-handle-multiple-error-types
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-handle-multiple-error-types/
---

# How to handle multiple error types



## The multiple error type problem

You're writing a function that reads a configuration file and extracts a port number. The file might not exist. The number inside might be garbage text. You write the function signature as `fn load_port() -> Result<u16, ???>`. The compiler stares back. You have two different error types waiting to happen, and Rust refuses to guess which one you mean.

This is the multiple error types problem. It shows up the moment your code touches more than one external crate or system call. A function can only return one error type. That type is part of the function's contract. If a function can fail in two ways, you need a single type that represents both possibilities.

## The solution: a custom enum

Rust functions return exactly one error type. That type is part of the function's contract. If a function can fail in two ways, you need a single type that represents both possibilities. The standard tool is a custom `enum`.

An enum acts like a tagged union. It holds one variant at a time. Each variant wraps a specific error type. You define the enum, list the variants, and implement the `Error` trait so the rest of the ecosystem treats it like any other error.

Think of it like a universal adapter. Your function has one output slot. The adapter has multiple input jacks. You plug the specific error into the correct jack, and the adapter presents a single unified shape to the caller.

## Minimal example

Here is the bare minimum to make multiple errors work. You define the enum, implement `Display` for user-facing messages, and implement `Error` to mark the type as a proper error.

```rust
use std::fmt;
use std::io;
use std::num::ParseIntError;

/// Wraps IO and parsing errors into a single type.
#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Parse(ParseIntError),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            // Format the inner error with a prefix.
            AppError::Io(e) => write!(f, "IO error: {}", e),
            AppError::Parse(e) => write!(f, "Parse error: {}", e),
        }
    }
}

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

fn read_and_parse() -> Result<u32, AppError> {
    // Simulate an IO error.
    let io_result: Result<String, io::Error> = Err(io::Error::new(io::ErrorKind::Other, "disk full"));
    // Convert io::Error to AppError::Io manually.
    let data = io_result.map_err(AppError::Io)?;
    // Simulate a parse error.
    let parse_result: Result<u32, ParseIntError> = "not_a_number".parse();
    // Convert ParseIntError to AppError::Parse manually.
    Ok(parse_result.map_err(AppError::Parse)?)
}
```

The enum `AppError` has two variants. Each variant holds a field of a specific type. This is called a tuple variant. When `read_and_parse` returns `Err(AppError::Io(...))`, the caller receives a single `AppError` value. The caller can match on it to see which variant it is.

The `Display` implementation controls how the error prints. The `Error` trait implementation is empty here, but it marks the type as a proper error. This allows you to use `?` on `Result<T, AppError>` in functions that return `Result<T, AppError>`. Without the `Error` trait, some crates refuse to accept your type.

Add `#[derive(Debug)]` to the enum. The compiler generates a debug representation automatically. The `Error` trait requires `Debug`, so this is mandatory. If you skip it, the compiler rejects the code with `E0277` (trait bound not satisfied) for the `Debug` trait.

## The `From` trait and automatic conversion

The manual `.map_err` calls in the minimal example work, but they clutter the code. Every error propagation requires a conversion. The community convention is to implement the `From` trait for each inner error type.

`From` defines a conversion from one type to another. Once you implement `impl From<X> for MyError`, the `?` operator handles the conversion automatically. You stop writing `.map_err` everywhere. The code reads like a straight line.

```rust
use std::fmt;
use std::io;
use std::num::ParseIntError;
use std::fs;

/// Errors for the config loader.
#[derive(Debug)]
enum ConfigError {
    Io(io::Error),
    Parse(ParseIntError),
}

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

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

// Allow automatic conversion from io::Error.
impl From<io::Error> for ConfigError {
    fn from(err: io::Error) -> Self {
        ConfigError::Io(err)
    }
}

// Allow automatic conversion from ParseIntError.
impl From<ParseIntError> for ConfigError {
    fn from(err: ParseIntError) -> Self {
        ConfigError::Parse(err)
    }
}

/// Loads the port from a file.
fn load_port(path: &str) -> Result<u16, ConfigError> {
    // Read the file. The ? operator uses From<io::Error> automatically.
    let content = fs::read_to_string(path)?;
    // Parse the number. The ? operator uses From<ParseIntError> automatically.
    let port = content.trim().parse()?;
    Ok(port)
}
```

The `?` operator checks the result. If it's an `Err`, the operator calls `From::from` to convert the error type to the function's return error type. If the conversion succeeds, the function returns early with the converted error. If no `From` implementation exists, the compiler rejects the code with `E0277` (the trait `From<X>` is not implemented for `MyError`).

Implement `From` for every variant. It saves you from writing `map_err` on every line. This pattern scales well. Add a new error type? Add a variant, implement `From`, and you're done.

## Error chains and source

When you wrap an error, you preserve the original context. The `Error` trait has a `source` method that returns the underlying cause. Implementing `source` allows tools to print the full chain of errors. This turns a flat error message into a debuggable stack.

```rust
impl std::error::Error for ConfigError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            ConfigError::Io(e) => Some(e),
            ConfigError::Parse(e) => Some(e),
        }
    }
}
```

The `source` method returns `Option<&(dyn Error + 'static)>`. It points to the inner error. If the error has no cause, return `None`. Most standard library errors implement `Error`, so they can be returned as the source.

Convention aside: manual `source` implementation is verbose. The ecosystem standard for custom errors is the `thiserror` crate. It provides a derive macro that generates `Display`, `Error`, `From`, and `source` implementations from annotations. You write less code and get fewer bugs.

Always implement `source`. It turns a flat error message into a debuggable chain.

## Pitfalls and compiler errors

Custom error enums introduce a few common traps. The compiler catches most of them, but the messages can be opaque if you don't know what to look for.

If you forget to implement `std::error::Error`, the compiler rejects code that expects an error trait. You'll see `E0277` (trait bound not satisfied) when passing your error to crates like `anyhow` or `eyre`. These crates require the `Error` trait to work.

If you forget `Display`, you can't print the error. The compiler complains with `E0277` regarding the `Display` trait. Many error handling crates require `Display` to format the error message.

If you use `?` without `From`, the compiler says "the trait `From<X>` is not implemented for `MyError`". You must add `map_err` or implement `From`. The error message points to the line with `?`.

If you mix error types in a match arm, the compiler rejects the code with `E0308` (mismatched types). Each arm of a match must return the same type. Ensure all branches return `MyError` or use `?` to propagate.

Trust the borrow checker. It usually has a point.

## Decision: when to use this vs alternatives

Rust offers several ways to handle errors. Choose the right tool based on your context.

Use a custom enum when you are building a library and want to expose specific error variants to callers. Callers need to match on the error to decide how to recover. The enum gives them precise control.

Use `anyhow` when you are writing an application binary and just need to propagate errors to the top level. You don't care about the specific variant; you just want to print the chain and exit. `anyhow` handles conversions automatically and requires zero boilerplate.

Use `thiserror` when you have a custom enum but want to avoid boilerplate. The derive macro generates `Display`, `Error`, and `From` implementations for you. It is the standard for library errors in the modern ecosystem.

Use `Box<dyn std::error::Error>` when you need a generic error type in a trait object or callback, and performance isn't critical. This erases the concrete type at a small runtime cost. It is useful for dynamic dispatch scenarios.

Libraries expose enums. Applications use `anyhow`. Derive macros handle the rest.

## Where to go next

- [How to handle errors in main function](/en/how-to-handle-errors-in-main-function/)
- [How to Use Result<T, E> for Error Handling in Rust](/en/how-to-use-resultt-e-for-error-handling-in-rust/)
- [How to Use map_err to Transform Errors in Rust](/en/how-to-use-maperr-to-transform-errors-in-rust/)