Skip to main content

Command Palette

Search for a command to run...

Rust Error Handling: thiserror, anyhow, and When to Use Each

Published
Rust Error Handling: thiserror, anyhow, and When to Use Each

In this blog post, we'll explore strategies for streamlining error handling in Rust using two popular libraries: thiserror and anyhow. We'll discuss their features, use cases, and provide insights on when to choose each library.

TL;DR

  • thiserror simplifies the implementation of custom error type, removing boilerplates

  • anyhow consolidates errors that implement std::error::Error

  • While thiserror provides detailed error information for specific reactions, anyhow hides internal details

Return Different Error Types from Function

Let's start by creating a function decode() for illustration. The function has 3 steps:

  1. Read contents from a file named input

  2. Decode each line as a base64 string

  3. Print each decoded string

The challenge is determining the return type for decode since std::fs::read_to_string(), base64 decode(), and String::from_utf8() each return different error types.

use base64::{self, engine, Engine};

fn decode() -> /* ? */ {
    let input = std::fs::read_to_string("input")?;
    for line in input.lines() {
        let bytes = engine::general_purpose::STANDARD.decode(line)?;
        println!("{}", String::from_utf8(bytes)?);
    }
    Ok(())
}

One approach is to use trait object: Box<dyn std::error::Error>. This works because all those types implement std::error::Error.

fn decode() -> Result<(), Box<dyn std::error::Error>> {
  // ...
}

While this is suitable in some cases, it limits the caller's ability to discern the actual error that occurred in decode(). Then, using enum is a good approach if it is desired to handle each error in different ways.

enum AppError {
    ReadError(std::io::Error),
    DecodeError(base64::DecodeError),
    StringError(std::string::FromUtf8Error),
}

By implementing std::error::Error trait, we can semantically mark AppError as an error type.

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

However, this code doesn't compile because AppError doesn't satisfy the constraints required by std::error::Error, implementation of Display and Debug:

error[E0277]: `AppError` doesn't implement `std::fmt::Display`
error[E0277]: `AppError` doesn't implement `Debug`

The definition of std::error::Error represents the consensus of minimum requirements of an error type in Rust. An error should have two forms of description for users (Display) and programmers (Debug), and should provide its root cause.

pub trait Error: Debug + Display {
    fn source(&self) -> Option<&(dyn Error + 'static)> { ... }
    // ...
}

The code will be like this after implementing the required traits:

impl std::error::Error for AppError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        use AppError::*;
        match self {
            ReadError(e) => Some(e),
            DecodeError(e) => Some(e),
            StringError(e) => Some(e),
        }
    }
}

impl std::fmt::Display for AppError { // Error message for users.
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        use AppError::*;
        let message = match self {
            ReadError(_) => "Failed to read the file.",
            DecodeError(_) => "Failed to decode the input.",
            StringError(_) => "Failed to parse the decoded bytes.",
        };
        write!(f, "{message}")
    }
}

impl std::fmt::Debug for AppError { // Error message for programmers.
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "{self}")?;
        if let Some(e) = self.source() { // <-- Use source() to retrive the root cause.
            writeln!(f, "\tCaused by: {e:?}")?;
        }
        Ok(())
    }
}

Finally, we can use AppError in decode():

fn decode() -> Result<(), AppError> {
    let input = std::fs::read_to_string("input").map_err(AppError::ReadError)?;
    // ...

map_err() is used to convert std::io::Error to AppError::ReadError. To use ? operator for better flow, we can implement From trait for AppError:

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

impl From<base64::DecodeError> for AppError {
    fn from(value: base64::DecodeError) -> Self {
        AppError::DecodeError(value)
    }
}

impl From<std::string::FromUtf8Error> for AppError {
    fn from(value: std::string::FromUtf8Error) -> Self {
        AppError::StringError(value)
    }
}

fn decode() -> Result<(), AppError> {
    let input = std::fs::read_to_string("input")?;
    for line in input.lines() {
        let bytes = engine::general_purpose::STANDARD.decode(line)?;
        println!("{}", String::from_utf8(bytes)?);
    }
    Ok(())
}

fn main() {
    if let Err(error) = decode() {
        println!("{error:?}");
    }
}

We did several things to use our custom error type fluently:

  • implement std::error::Error

  • implement Debug and Display

  • implement From

These can be verbose and tedious, but fortunately, thiserror automatically generates most of them.

Remove Boilerplates with thiserror

The code above is simplified using thiserror:

#[derive(thiserror::Error)]
enum AppError {
    #[error("Failed to read the file.")]
    ReadError(#[from] std::io::Error),
    #[error("Failed to decode the input.")]
    DecodeError(#[from] base64::DecodeError),
    #[error("Failed to parse the decoded bytes.")]
    StringError(#[from] std::string::FromUtf8Error),
}

impl std::fmt::Debug for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "{self}")?;
        if let Some(e) = self.source() {
            writeln!(f, "\tCaused by: {e:?}")?;
        }
        Ok(())
    }
}

#[error] macro generates Display, #[from] macro handles From implementations and source() for std::error::Error. The implementation of Debug remains to provide detailed error messages, but #derive[Debug] can also be used if it's enough:


// The manual implementation of Debug
Failed to decode the input.
        Caused by: InvalidPadding

// #[derive(Debug)]
DecodeError(InvalidPadding)

Deal with Any Error with anyhow

anyhow offers an alternative method for simplifying error handling, which is similar to Box<dyn std::error::Error>> approach:

fn decode() -> Result<(), anyhow::Error> {
    let input = std::fs::read_to_string("input")?;
    for line in input.lines() {
        let bytes = engine::general_purpose::STANDARD.decode(line)?;
        println!("{}", String::from_utf8(bytes)?);
    }
    Ok(())
}

It compiles since types implementing std::error::Error can be converted to anyhow::Error. The error message will be like:

Invalid padding

For enhanced error messages, context() can be used:

let bytes = engine::general_purpose::STANDARD
    .decode(line)
    .context("Failed to decode the input")?;

Then, the error message will be:

Failed to decode the input

Caused by:
    Invalid padding

Now our error handling is streamlined thanks to the anyhow's type conversion and context().

Comparison between thiserror and anyhow

While thiserror and anyhow might seem similar, they serve different purposes. thiserror is suitable when users need to react differently based on the actual error type. On the other hand, anyhow is effective when internal details can be hidden from the user.

In this sense, it's often said that thiserror is for a library, and anyhow is for an application. This saying is true to some extent, considering that library developers tend to want to give precise information to users (programmers), and applications don't have to show detailed error information to their users.

Conclusion

In conclusion, we've explored the distinctive features of thiserror and anyhow and discussed scenarios where each library shines. By choosing the right tool for the job, Rust developers can significantly simplify error handling and enhance code maintainability.

  • thiserror simplifies the implementation of custom error types

  • anyhow integrates any std::error::Error

  • thiserror is ideal for library development where detailed information is beneficial for users (programmers).

  • anyhow is Suited for applications where internal details are not crucial, providing simplified information to users.

More from this blog

キャリア 6 年目の振り返り

2020 年 4 月に働き始めてから 6 年近く経過した。今年を振り返ってまとめておく。 正社員化 2025 年前半あたりは、生活に安定感がなくジャグリングをしているような感覚だった。学校の卒業、ビザの切り替え、インターンから正社員への移行という3つのイベントを同時に進めていた。結果的に 5 月から正社員として働き始めたが、計画通りには行かなかった。 会社の人事プロセスが大幅に遅延した。本来は 4 月から働き始める予定だったのに、一ヶ月以上遅れて 5 月になってしまった。これに関しては、本当にこ...

Dec 29, 2025
キャリア 6 年目の振り返り

キャリア 5 年目の振り返り

2020 年 4 月に働き始めてから 5 年が経った。本当は毎年 12 月に一年の振り返りをしようと思っていたのだが、今回はいろいろあって 4 月になってしまったので、2024 年 1 月から 2025 年の今までを対象として振り返りをして、今年の目標を書こうと思う。 海外就職 カナダに渡航して約 1 年半経った。最初の一年間は private college という日本の専門学校のようなところに通っていて、それからジョブオファーを得て働き始めた。 海外で働いてキャリアを積むというのがカナダに来...

Apr 14, 2025
キャリア 5 年目の振り返り

ReRe: Recursive Redefinition

38 posts