Skip to Content
Course content

159: Designing Error Types with thiserror

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're managing a high-end restaurant kitchen. If a cook yells, "Something went wrong!" that's useless. You don't know if the oven caught fire, the fish is spoiled, or someone just dropped a plate. You need specific labels. You need to know: "The oven is at the wrong temperature" (and exactly what that temperature is) or "We are out of sea bass."

In Rust, using a generic Box or a string is like that vague shout of "Something went wrong!" It tells the caller that an error happened, but it doesn't give them a way to react programmatically. thiserror is essentially a way to create a professional, categorized "labeling system" for your errors without spending hours writing the same tedious boilerplate code for every single error type.

Ditching the Display Boilerplate

If you've built a custom error enum before, you know the drill. You define the enum, then you implement std::fmt::Display to make it printable, and then you implement std::error::Error. It's a lot of typing for very little reward. I've spent more time than I'd like to admit writing write!(f, "Configuration file not found") over and over.

With thiserror, we move that logic directly onto the enum variants using the #[error("...")] attribute. Let's look at a real scenario: a small utility that reads a user's configuration file and parses a specific API key.

use thiserror::Error;
use std::io;
use std::num::ParseIntError;

#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("could not read configuration file: {0}")]
    Io(#[from] io::Error),

    #[error("api key must be a number, but found: {0}")]
    InvalidKeyFormat(#[from] ParseIntError),

    #[error("missing required configuration key: {0}")]
    MissingKey(String),

    #[error("the configuration file is empty")]
    EmptyFile,
}

Handling the "Wrapped" Errors

Notice those #[from] attributes? That's where the real magic happens. In the example above, ConfigError::Io doesn't just label the error; it wraps a standard std::io::Error.

Because of #[from], thiserror automatically implements From<io::Error> for ConfigError. This means if you're inside a function that returns Result<T, ConfigError>, you can use the ? operator on a function that returns an io::Error, and Rust will automatically convert it into your ConfigError::Io variant. It keeps your business logic clean while preserving the original error's context.

When to choose this over anyhow

You've likely seen anyhow in other projects. I get asked a lot: "Why bother with thiserror if anyhow is easier?"

Here is the rule of thumb I follow: Use anyhow for applications (the final binary), and use thiserror for libraries. If you're writing a crate that other people will use, you cannot just return an anyhow::Error. Why? Because the person using your library might want to programmatically check if the error was a MissingKey so they can prompt the user to enter it. If you wrap everything in a generic anyhow blob, you've robbed your user of the ability to handle specific errors gracefully.

By using thiserror, you provide a strongly-typed contract. You're telling the user: "Here are the five specific things that can go wrong in my code, and here is exactly how to identify them."




📋 Practical Task

Building a Secure Vault Error Handler

You are building a "Secure Vault" module that manages encrypted secrets. You need to create a custom error type called VaultError using the thiserror crate that handles the following scenarios:

  • Disk Failure: Should wrap a std::io::Error using #[from].
  • Decryption Failure: Should wrap a std::num::ParseIntError (simulating a failed key parse) using #[from].
  • Permission Denied: A custom error that takes a String representing the username who was denied access. The error message should be: "User {0} does not have access to this vault".
  • Vault Locked: A simple unit variant with the message: "The vault is currently locked by another process".

Requirements:

  1. Implement the VaultError enum with the appropriate thiserror derives and attributes.
  2. Write a function open_vault(user: &str) -> Result<String, VaultError> that returns a VaultError::PermissionDenied if the username is not "admin", and a VaultError::VaultLocked if a simulated lock is present.
  3. Demonstrate that a std::io::Error can be converted into a VaultError using the ? operator inside a function.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.