Error handling for applications: how to return a public message error instead of all the chain of errors and tracing it at the same time?

PROLOGUE

I'm using async-graphql and I have hundreds of resolvers and for each resolver I would like to trace all the possible errors.

In each method of my app I'm using anyhow::{Error}.

Right now I have code similar to this for each resolver:

#[Object]
impl MutationRoot {
    async fn player_create(&self, ctx: &Context<'_>, input: PlayerInput) -> Result<Player> {
        let services = ctx.data_unchecked::<Services>();

        let player = services
            .player_create(input)
            .await?;

        Ok(player)
    }
}

So I thought about using the below code (note the added line with .map_err()):

#[Object]
impl MutationRoot {
    async fn player_create(&self, ctx: &Context<'_>, input: PlayerInput) -> Result<Player> {
        let services = ctx.data_unchecked::<Services>();

        let player = services
            .player_create(input)
            .await
            .map_err(errorify)?;

        Ok(player)
    }
}

fn errorify(err: anyhow::Error) -> async_graphql::Error {
    tracing::error!("{:?}", err);

    err.into()
}

Now the error is traced along with all the error chain:

ERROR example::main:10: I'm the error number 4

Caused by:
    0: I'm the error number 3
    1: I'm the error number 2
    2: I'm the error number 1
    in example::async_graphql

QUESTION 1

Is there a way to avoid the .map_err() on each resolver?

I would like to use the ? alone.

Should I use a custom error?

Can we have a global "hook"/callback/fn to call on each error?

QUESTION 2

As you can see above the chain of the error is the inverse.

In my graphql response I'm getting as message the "I'm the error number 4" but I need to get the "I'm the error number 2" instead.

The error chain is created using anyhow like this:

  • main.rs: returns error with .with_context(|| "I'm the error number 4")?
    • call fn player_create() in graphql.rs: returns with .with_context(|| "I'm the error number 3")?
      • call fn new_player() in domain.rs: returns with .with_context(|| "I'm the error number 2")?
        • call fn save_player() in database.rs: returns with .with_context(|| "I'm the error number 1")?

How can I accomplish this?

I'm really new to Rust. I come from Golang where I was using a struct like:

type Error struct {
	error          error
	public_message string
}

chaining it easily with:

return fmt.Errorf("this function is called but the error was: %w", previousError)

How to do it in Rust?

Do I necessarily have to use anyhow?

Can you point me to a good handling error tutorial/book for applications?

Thank you very much.

I'm not sure I would use anyhow if you need to retrieve information from the error for anything other than debugging. You can use the Error::source method to go down the chain to find the actual error, but you'll have to essentially guess and check to try and find the right error. That may be fragile.

I generally prefer to use an error enum for situations where I need to return an API error or something like that, possibly with thiserror for convenience.

Here's a simple example of what that might look like

Playground

use std::{fmt::Debug, num::ParseIntError};

use backtrace::Backtrace;

// Add the different error types your app can generate to this enum
#[derive(Debug, thiserror::Error)]
enum ErrorKind {
    #[error("IO: {0}")]
    IO(#[from] std::io::Error),

    #[error("ParseInt: {0}")]
    ParseInt(#[from] ParseIntError),
}

// Add context to your errors.
struct MyError {
    kind: ErrorKind,
    backtrace: Backtrace,
}

impl Debug for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "{}\n", self.kind)?;
        writeln!(f, "{:?}", self.backtrace)?;

        Ok(())
    }
}

// Convert from any error that can be converted into ErrorKind into MyError
impl<T> From<T> for MyError
where
    T: Into<ErrorKind>,
{
    fn from(from: T) -> Self {
        MyError {
            kind: from.into(),
            backtrace: Backtrace::new(),
        }
    }
}

fn main() -> Result<(), MyError> {
    one()
}

fn one() -> Result<(), MyError> {
    two()
}

fn two() -> Result<(), MyError> {
    three()
}

fn three() -> Result<(), MyError> {
    four()
}

fn four() -> Result<(), MyError> {
    let value: i32 = "one".parse()?;

    println!("{value}");

    Ok(())
}

thiserror automates generating the Display, and Error impls, and the #[from] annotations cause it to also generate From impl's for the annotated type.

Using another container type makes it easy to add additional context data to the errors. You could easily set up a context system like anyhow does with a wrapper type like that.

The advantage of this strategy is that you maintain full control of what information you have access to at any point in the chain of errors. The downside is that you may have to reimplement some conveniences like pretty printed terminal output.

1 Like

Another option, more, so to speak, "full-stack", is snafu, which I think[1] covers the features of both anyhow and thiserror.


  1. I'm familiar with anyhow and thiserror only by name. ↩ī¸Ž

1 Like

I think this is what I need. Do you have article/post/tutorial/link for me to further study?

Thank you!

Unfortunately I don't, it's mostly based on my own experimenting with error handling.