---
title: "12.6 Custom error types: a first look"
description: "Building error types as enums so callers can inspect what went wrong, implementing Display, and the Box<dyn Error> escape hatch for when you just want errors to flow."
url: "https://learnrust.net/chapter-12/custom-error-types/"
last_updated: "2026-06-13"
---

# 12.6 Custom error types: a first look

Every error in this chapter has been a `String`. That's the simplest possible error type, and it has a real limit: a `String` error can only be *printed*. A caller can't ask "was this a not-found error or a permission error?" and respond differently, because all they have is text. This lesson shows the sturdier approach, an error type built from a chapter 11 enum, and then the pragmatic escape hatch for when defining a type is more effort than the situation warrants. Both lean on chapter 16 (traits) for their full story, so we'll show the shapes and name the forward references honestly.

## Why a String error isn't enough

Consider a function that parses an age and can fail two ways: the text isn't a number, or the number is unreasonable. With `String` errors, the caller is stuck:

```rust
fn parse_age(text: &str) -> Result<u32, String> {
    let n: u32 = text.parse().map_err(|_| String::from("not a number"))?;
    if n > 150 {
        return Err(String::from("unreasonably large"));
    }
    Ok(n)
}
```

If the caller wants to treat "not a number" differently from "too large", say, retry on one but reject outright on the other, they'd have to compare error *strings*, which is fragile and ugly. The error has structure (it's one of two kinds), but `String` threw that structure away. This is the lesson [11.1](https://learnrust.net/chapter-11/enums/) situation exactly: a value that is one of a fixed set of kinds wants to be an enum.

## An error enum

Define the error as an enum, one variant per way the operation can fail, carrying any detail each kind needs:

```rust
#[derive(Debug)]
enum AgeError {
    NotANumber,
    TooLarge(u32),
}

fn parse_age(text: &str) -> Result<u32, AgeError> {
    let n: u32 = text.parse().map_err(|_| AgeError::NotANumber)?;
    if n > 150 {
        return Err(AgeError::TooLarge(n));
    }
    Ok(n)
}

fn main() {
    for input in ["42", "abc", "999"] {
        match parse_age(input) {
            Ok(age) => println!("{input}: age {age}"),
            Err(AgeError::NotANumber) => println!("{input}: not a number"),
            Err(AgeError::TooLarge(n)) => println!("{input}: {n} is too large"),
        }
    }
}
```

```
42: age 42
abc: not a number
999: 999 is too large
```

Now the caller can `match` on the error and respond per kind, with full exhaustiveness checking (lesson [11.1](https://learnrust.net/chapter-11/enums/)): add a third failure mode later and every caller's `match` is forced to address it. The `TooLarge(n)` variant even carries the offending number, so the handler can mention it. This is the chapter 11 toolkit aimed at errors, and it's how serious Rust programs model failure: an enum of the things that can go wrong, inspectable by whoever catches it.

## Making it printable: Display

The error enum derives `Debug`, so `{:?}` works (lesson [10.7](https://learnrust.net/chapter-10/deriving-debug/)). For a *human-facing* message, the one a user reads, you implement `Display`, the `{}` formatter that lesson [10.7](https://learnrust.net/chapter-10/deriving-debug/) said you'd write yourself. Here's the shape, with the trait machinery flagged as chapter 16's job:

```rust
use std::fmt;

#[derive(Debug)]
enum AgeError {
    NotANumber,
    TooLarge(u32),
}

impl fmt::Display for AgeError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AgeError::NotANumber => write!(f, "input was not a number"),
            AgeError::TooLarge(n) => write!(f, "{n} is too large to be an age"),
        }
    }
}

fn main() {
    let e = AgeError::TooLarge(999);
    println!("{e}");    // uses Display
    println!("{e:?}");  // uses Debug
}
```

```
999 is too large to be an age
TooLarge(999)
```

`impl fmt::Display for AgeError { ... }` is implementing a trait by hand, which is genuinely chapter 16's subject, so don't worry about the exact syntax yet. The takeaway is the division of labor you met in lesson [10.7](https://learnrust.net/chapter-10/deriving-debug/), now for errors: `Debug` (`{:?}`) is the derivable programmer's view, `Display` (`{}`) is the human-facing message you write because only you know how the error should read in a sentence. A well-made error type usually has both.

> **Key insight**
>
> An error type is just a type, and the same chapter 10 and 11 tools build it: an enum when failure comes in distinct kinds, `Debug` derived for inspection, `Display` implemented for the human message. There's no separate "exception" language feature to learn (lesson [12.1](https://learnrust.net/chapter-12/panic-and-unrecoverable-errors/)'s ledger: Rust replaces C++'s entire exception chapter with "errors are values of types you define"). What chapter 16 adds is the `Error` trait that lets all these custom types interoperate.

## The escape hatch: Box<dyn Error>

Defining a tidy enum is the right move for a library or a long-lived program. But sometimes, especially in a small application or a quick `main`, you have several *different* error types flowing through one function, parse errors and file errors and your own errors, and writing a grand enum to unify them is more ceremony than the program deserves. For that, Rust offers a catch-all error type:

```rust
use std::error::Error;

fn run() -> Result<(), Box<dyn Error>> {
    let n: i32 = "42".parse()?;
    let m: i32 = "100".parse()?;
    println!("{}", n + m);
    Ok(())
}

fn main() -> Result<(), Box<dyn Error>> {
    run()
}
```

```
142
```

`Box<dyn Error>` means, roughly, "a pointer to some error, any error." A function returning `Result<T, Box<dyn Error>>` accepts `?` on *any* error type, because every standard error can become a `Box<dyn Error>` automatically. It's the pragmatic choice when you want errors to propagate cleanly and you don't need callers to distinguish kinds, just to know something failed and see the message. The cost is exactly that lost distinguishing: you've traded the enum's inspectability for the convenience of not defining one.

The pieces here, `dyn`, `Box`, the `Error` trait, are real topics: `Box` is a heap pointer (chapter 21), `dyn Error` is a trait object (chapter 16). We show `Box<dyn Error>` now, shown-not-explained, because it's the single most useful error type for application `main`s and you'll want it long before chapter 16. Treat it as a working incantation: "the error type that accepts any error", paired with `?` and a `main` that returns `Result` (lesson [12.3](https://learnrust.net/chapter-12/propagating-errors-question-mark-operator/)).

> **Best practice**
>
> For applications and quick programs, reach for `Box<dyn Error>` and let `?` carry everything to a `main` that returns `Result`. For libraries and code where callers need to react to specific failures, define an error enum with `Debug` and `Display`. The rule of thumb mirrors lesson [12.5](https://learnrust.net/chapter-12/when-to-panic/): the more your code is reused, the more its errors should be inspectable types rather than an opaque box.

## Quiz time

**Question #1**

Why might a caller prefer an `AgeError` enum over a `String` error?

<details class="solution">
<summary>Show solution</summary>

An enum lets the caller `match` on the *kind* of error and respond differently to each (retry on one, reject on another), with exhaustiveness ensuring new error kinds are handled. A `String` error can only be printed; its structure is gone, so the caller would have to compare error text, which is fragile. The enum keeps the failure's structure inspectable.

</details>

**Question #2**

What's the difference between deriving `Debug` and implementing `Display` for an error type, and why is only one of them derivable?

<details class="solution">
<summary>Show solution</summary>

`Debug` (`{:?}`) is the programmer-facing structural dump, derivable because "show the variant and its data" has an obvious form. `Display` (`{}`) is the human-facing message you implement yourself, because only you know how the error should read in prose, so the compiler can't generate it. Error types usually have both: `Debug` for logs and `Display` for users. (This is lesson [10.7](https://learnrust.net/chapter-10/deriving-debug/)'s division, applied to errors.)

</details>

**Question #3**

When would you use `Box<dyn Error>` instead of a custom error enum?

<details class="solution">
<summary>Show solution</summary>

When several different error types flow through one function and you don't need callers to tell them apart, just to propagate them and report a message, typically in an application or a quick `main`. `Box<dyn Error>` accepts `?` on any error type with no enum to define. Use a custom enum instead when callers need to react to specific failure kinds, as in a library.

</details>

That's the full error-handling toolkit: panic for bugs, `Result` for recoverable failures, `?` to propagate, and error types built from the same structs and enums you already know. The chapter summary and a capstone quiz are next, and then chapter 13 finally answers a question that's been quietly growing: how do you split a program across multiple files and keep its names from colliding?

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
