Ramblings of an aging IT geek
← Ramblings of an aging IT geek
rust

rust errors back when you wrote every From by hand

Looking back at how we handled errors in Rust before the crate ecosystem settled, and why the boilerplate taught me something worth keeping.

A code editor showing Rust enum definitions

I dug an old Rust project out this weekend and was reminded how much work error handling used to be. These days you reach for one crate for libraries and another for applications and the question is more or less solved. Back when I wrote this, the convention had not settled, and so every project carried its own hand-built error machinery. It was tedious. It was also, I have come to think, quietly educational in a way the modern convenience has slightly papered over.

The shape of a hand-rolled error

The pattern was always the same. You defined an enum with a variant for every kind of failure your code could produce, you implemented Display so it printed something sensible, you implemented std::error::Error, and then, the real labour, you wrote a From implementation for every underlying error type you wanted the ? operator to convert automatically.

use std::fmt;

#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    NotFound(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "io error: {}", e),
            AppError::Parse(e) => write!(f, "parse error: {}", e),
            AppError::NotFound(s) => write!(f, "not found: {}", s),
        }
    }
}

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

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self {
        AppError::Io(e)
    }
}

impl From<std::num::ParseIntError> for AppError {
    fn from(e: std::num::ParseIntError) -> Self {
        AppError::Parse(e)
    }
}

That is a lot of ceremony to be able to write let n: i32 = s.parse()?; and have it just work. Every new error source you wanted to bubble up meant another variant and another From, and you wrote that block in essentially every project, by hand, slightly differently each time.

Why the boilerplate was not entirely wasted

Here is the thing, though. Writing all that out by hand made the error model impossible to ignore. You could not add a failure case without naming it, deciding how it printed, and deciding how it converted. The enum became an honest catalogue of everything your code could go wrong doing, and you read it like documentation, because in effect it was.

The ? operator is the bit that made the whole arrangement worth the trouble. Once the From conversions existed, the happy path read cleanly and the conversions happened invisibly at each ?. The boilerplate was concentrated in one place, the error module, and the rest of the code stayed tidy. That separation, all the ugliness in one file so the logic elsewhere can breathe, is a pattern I still reach for even when a macro could hide the ugliness entirely.

There were rough edges I do not miss. Two libraries each with their own error enum did not compose, so you ended up wrapping one in the other, writing yet more From implementations to bridge them. Box up a dyn Error to escape that and you lost the typed matching that was the point of the enum in the first place. The tension between "I want concrete, matchable error types" and "I just want to propagate this and add a bit of context" was the unsolved problem of the era, and it is exactly the gap the newer crates have since filled, one taking each side.

What I keep from it

The convenience is better. I am not nostalgic for typing From implementations at midnight. But hand-rolling errors taught me to treat the set of ways my code can fail as a thing worth designing rather than a thing that happens to me, and that habit survives the boilerplate going away. When I lean on a derive macro now I still pause and ask what the enum would have looked like if I had written it out, because that question is the useful part and the macro was only ever the typing. The tools got better. The thinking they used to force on us is the bit worth keeping.