---
title: 'Custom Error Types'
source: 'https://academia.sh/en/courses/rust-abstraction/custom-error-types'
course: 'Traits, Generics and Error Handling'
language: en
updated: '2026-09-15T04:58:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows by executing that the question mark runs an error conversion without the caller writing anything'
    - 'Verifies by counting in how many links of an error chain the conversion actually ran'
    - "Distinguishes by comparison with a hand-written conversion that the implicit conversion loses the caller's context information"
---

# Custom Error Types

Measuring that the question mark converts one error type into another without the caller writing any conversion call, and that this implicit conversion — unlike a hand-written one — cannot carry context information from the caller.

The Shorthand for Error Propagation lesson showed that the question mark converts
different error types into each other, but it did not go into *how* that conversion
happens — it only used it. This lesson opens up that conversion: when an error moves
from one type to another, who calls that transition, and is that call visible in the
caller's source?

The `impl From<std::num::ParseIntError> for NumberError` block set up in the previous
lesson is the piece that unlocks this question. That lesson used this block only as a
precondition needed for the question mark to work; this lesson examines that same block
*itself* — what is built inside it, what is discarded, and who triggers that
construction, how many times.

The Interfaces, Errors and Generics course measured error wrapping and the error
chain — whether an error carries another error inside it, how an error's identity is
preserved through a chain. This lesson measures not wrapping but **conversion**: an
error type moves forward not by keeping its own form, but by being converted entirely
into another type. Rust keeps these two mechanisms separate — wrapping was that course's
subject, conversion is this lesson's.

Wrapping puts an error inside an envelope; inside that envelope, the original error stays
intact and can be taken back out when needed. Conversion, by contrast, destroys the
original error: the source type is given up, a new type takes its place, and that new
type can no longer turn back into the old one — unless it stored the old one inside
itself as a field. This lesson measures this second mechanism with an example where
`NumberError` does not store `ParseIntError` inside itself, it only reduces it to a
signal. The `From` implementation shows where this reduction happens: the source error's
data is either kept or discarded while the target type is being built — that is the
decision of whoever writes the implementation.

This topic's first three lessons measured how many times `Option`'s and `Result`'s own
success and failure arms ran; the error values inside those arms did not matter *in
themselves*, `NumberError` was an empty struct carrying no information. This lesson
fills that emptiness: `NumberError` now carries a `field`, and whether that field is
filled correctly or not depends on where and how the conversion is done.

The word "custom" in the lesson title points to an error type designed to be specific to
its source: `NumberError` is not part of the standard library, it is a struct built to
carry this program's own form of failure. In a real program, such a type could also be an
enum instead of a single field — each variant could carry a different kind of failure (a
field missing entirely, a number out of range, a format mismatch). This lesson does not
build that variety; with a single struct and a single field, it focuses on the question
of who calls the conversion. Each variant of a diversified error type could use the same
`From` mechanism too; the axis being measured does not change.

## Who Calls It: Implicit Conversion

If an error type can be produced from another error type through a `From`
implementation, the question mark calls that production automatically. There is no line
in the caller's source showing that call — `?` is the only character standing there.

The run below stands in for a small chain that parses a three-field record: the `age`,
`score`, and `attempt` fields are each checked in turn by the `link` function. The chain
itself is linear — one field does not wait for another, all are called
independently — but the word "chain" here describes the sum of three separate links: how
many links the conversion actually ran in is read from the total of the three.

```rust
// conversion.rs — the NumberError/From slice of times.rs: how many links the conversion ran in
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;

fn record(ledger: &Ledger, name: &str) {
    *ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}
fn times(ledger: &Ledger, name: &str) -> u32 {
    *ledger.borrow().get(name).unwrap_or(&0)
}

struct NumberError {
    field: String,
}

impl From<std::num::ParseIntError> for NumberError {
    fn from(_: std::num::ParseIntError) -> NumberError {
        NumberError { field: String::from("unknown") }
    }
}

fn link(_field: &str, text: &str) -> Result<i32, NumberError> {
    let number: i32 = text.parse()?;
    Ok(number)
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
    let entries = [("age", "thirty"), ("score", "78"), ("attempt", "three")];

    for (field, value) in entries {
        match link(field, value) {
            Ok(number) => println!("{:<8} success: {}", field, number),
            Err(error) => {
                record(&ledger, "error-conversion");
                println!("{:<8} failure, recorded field: {}", field, error.field);
            }
        }
    }
    println!();
    println!("links in the chain: {}", entries.len());
    println!("links where conversion ran: {} times", times(&ledger, "error-conversion"));
}
```

```
age      failure, recorded field: unknown
score    success: 78
attempt  failure, recorded field: unknown

links in the chain: 3
links where conversion ran: 2 times
```

**EH10.** No line in `link`'s body other than `text.parse()?` mentions
`NumberError` — the `ParseIntError` that `parse` produces automatically converts into
`link`'s return type, `NumberError`, because an `impl From<ParseIntError>` is written for
`NumberError`. Two of the three links in `entries` (`"age"` and `"attempt"`) cannot be
parsed, and `error-conversion` increases twice; because `"score"` can be parsed,
conversion never runs in that link. While the number of links in the chain stays fixed
(three), how many links the conversion actually ran in depends on the input — here, two
out of three.

**EH11.** Both printed `field` values are `"unknown"` — even though the fields that
actually failed are `"age"` and `"attempt"`, the error value itself does not know this.
The `From` implementation's signature is fixed: it only takes a `ParseIntError`, it does
not know which `field` argument `link` was called with. The conversion really does
run — the `error-conversion` counter proves it — but the caller (the `?` inside `link`)
cannot pass any context to this conversion, because what calls the conversion is not
`link` itself but the code the compiler generates for `?`.

The only place in `link`'s body that mentions `NumberError` is the return type
(`Result<i32, NumberError>`); the body itself only writes `text.parse()?`. This is the
same benefit of the shorthand measured in the previous lesson — the source stays
short — but its cost shows up here too: as the source shortens, the information that
source *can call on* narrows as well. Whoever calls `link` knows the `field` (the first
element in `entries`), but `link`'s own body carries no way to pass that information on
to `NumberError`, because the passing happens behind `?`, outside `link`'s control.

Both increases in `error-conversion` fall into the "every time" class: for every field
with invalid input, the conversion runs unavoidably — not conditionally, not
probabilistically, if the input is invalid the conversion runs with certainty. This is
different from the "only when needed" class seen with things like `lazy-default-needed`
or `after-question-success` in previous lessons: there, the count could be zero or one
depending on the input; here, for every failed link, the conversion runs exactly once,
unconditionally.

The three elements in the `entries` array are processed in the same order on every
repetition of the run; changing that order does not change the `error-conversion` count,
because each link is evaluated independently — an earlier link succeeding or failing has
no effect on the next link's result. This continues the same pattern introduced as a
validation pipeline in the previous lesson: here it additionally counts whether each link
ran a conversion too. The resulting count of "2 times" says, together, exactly how many
of the three links failed and that the conversion was actually triggered in each of
those failures.

## A Limit on the Measurement

Every error conversion can lose information. If the converted-to type does not carry the
source, the lost information cannot be brought back unless it is carried by hand. This is
not a specific shortcoming of `NumberError`, it is a natural consequence of the `From`
signature: `fn from(value: Source) -> Target` only takes the value of type `Source`, it
sees no variable from around where it was called. This limit is the same for every
`From` implementation — it only becomes visible for types like `NumberError` that need
information *outside* the source error (such as the `field` name). The run below repeats
the same scenario, writing the conversion by hand.

```rust
// manual_conversion.rs — the caller writes the conversion explicitly, losing no context
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;

fn record(ledger: &Ledger, name: &str) {
    *ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}
fn times(ledger: &Ledger, name: &str) -> u32 {
    *ledger.borrow().get(name).unwrap_or(&0)
}

struct NumberError {
    field: String,
}

fn link_by_hand(field: &str, text: &str) -> Result<i32, NumberError> {
    text
        .parse::<i32>()
        .map_err(|_| NumberError { field: field.to_string() })
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
    let entries = [("age", "thirty"), ("score", "78"), ("attempt", "three")];

    for (field, value) in entries {
        match link_by_hand(field, value) {
            Ok(number) => println!("{:<8} success: {}", field, number),
            Err(error) => {
                record(&ledger, "error-conversion");
                println!("{:<8} failure, recorded field: {}", field, error.field);
            }
        }
    }
    println!();
    println!("links in the chain: {}", entries.len());
    println!("links where conversion ran: {} times", times(&ledger, "error-conversion"));
}
```

```
age      failure, recorded field: age
score    success: 78
attempt  failure, recorded field: attempt

links in the chain: 3
links where conversion ran: 2 times
```

**EH12.** The counts are exactly the same: conversion runs in two of the three links. But
the `field` values are now correct — when `"age"` fails, the error prints `"age"`; when
`"attempt"` fails, the error prints `"attempt"`. The difference is that the closure given
to `map_err` can *capture* the `field` parameter: the `field` variable in
`link_by_hand`'s own scope is reachable from inside the closure. `From::from` does not
allow such a capture — its signature only takes the source error, it takes nothing from
the calling context. This is concrete proof of why the shorthand (`?`) sometimes falls
short: the shorthand is fast and short, but it can only carry as much information as the
target type can produce from the source error; if it needs to carry context *outside*
the source error (here, the `field` name), a hand-written conversion is needed.

This limit points to a general rule: a conversion is irreversible if the target type does
not hold the source or its context. `NumberError` having a `field` makes this loss
visible — if it had been an empty struct (as in the previous two lessons), there would
have been nothing to lose, because there was no information to carry in the first place.
When an error type is designed to carry information, this design decision comes along
with it: an implicit conversion, or a hand-written one that preserves context?

The difference between `link` and `link_by_hand` is a variation on the "same count,
different source length" example seen in the previous two lessons. There, the shorthand
and the hand-written form gave the same count and the difference was only in line count;
here too the count stays the same, but the difference is no longer in the line count, it
is in the *information the error carries*. The brevity the shorthand wins this time comes
with a cost — and that cost becomes visible not through the run counter, but through the
printed `field` value.

The count alone is not a sufficient criterion when choosing between these two
designs — both runs gave the same "links where conversion ran: 2 times" line. The
decision lies between how short the source will stay and how much information the error
will carry. `link`, with its one-line body, represents the shortest form, but its error
always says `"unknown"`. `link_by_hand` carries a three-line `map_err` call, but its
error names the correct field. A codebase can contain both forms: if which field an
error came from does not matter (if just "something failed" is enough), the shorthand can
be preferred; if the error's source will be shown to a user or a log record, a
hand-written conversion is needed.

The same implicit/manual distinction also raises a question about the conversion's
*cost* — but this lesson does not measure that cost. `From::from`'s body here amounts to
building a single struct; `map_err`'s closure does the same job. How many times each of
them ran was measured above (two), and each body is the same size. The only difference is
in *what* gets built: one builds a fixed string, the other a string coming from the
calling context.

The Trait Definition and Implementation lesson measured that a trait's default body
might never run on one type and might run on another — two different implementations of
the same contract could give two different run counts. `From<ParseIntError> for
NumberError` is a trait implementation too, and this lesson carries the same principle:
the implementation is written once in the source, but in a run it can be called zero or
more times depending on the input. The difference is that this implementation's call
never appears explicitly in the source — it stays behind `?`.

## Summary

- If an error type can be produced from another error type with `From`, the question
  mark calls that production automatically; no line in the caller's source shows that
  call.
- Which links in a chain the conversion actually ran in can be counted; here the
  conversion runs **every time** in two of the three links — unavoidable for every
  failed link, not conditional.
- `From::from`'s signature only takes the source error; it can take no information from
  the calling context (such as which field failed).
- A hand-written conversion (a closure with `map_err`) can preserve this information
  because it can capture variables from the calling scope — it gives the same count
  while carrying more information.
- Every error conversion can lose information; if the target type does not carry the
  source or its context, this loss cannot be reversed.
- The choice is between the source's brevity and the richness of the information the
  error carries; whichever is chosen, the count of how many times the conversion runs
  stays the same.

## Next Step

This topic's first four lessons measured the situations where an operation *might*
fail — `Option`'s absence, `Result`'s arms, the shorthand's silent skipping, the
conversion's loss of information. One thing is common to all of them: the caller took
the failure as a value and decided for itself what to do — it continued, used a
fallback, returned early, carried the information, or lost it. But the decision was
always the caller's.

The Panic and Unrecoverable Errors lesson measures the exact opposite: a design where a
failure, without producing any value, without leaving the caller any choice, stops the
remaining body outright. A function returning `Result` presents its failure as data and
even lets the caller ignore it; the design measured in this topic's last lesson, on the
contrary, models a stop that cannot be ignored — and it compares how the remaining-body
counts the two designs give for the same failure diverge.
