Help with error propagation

Hi,
I'm trying to improve my error handling (coming from Python this is really a handful) and I have this situation:

fn foo() -> Result<bar, Box<dyn Error>> {
    ...
    let err_vec: Vec<String> = vec!();

    if foo {
        err_vec.push("bar")
    }

    if foo2 {
        err_vec.push("bar2")
    }

    if !err_vec.is_empty() {
        return Err(err_vec.join(", ").into());
    }
}

fn caller() -> Result<bla, Box<dyn Error>> {
    match foo {
    Ok(x) => {do stuff},
    Err(e) => {if let Some(err) = e.downcast_ref() {
        [store err and append to other possible errors and eventually return all]}}
    } else {return Err(e)}
}

So I want to propagate the error from foo, check for the error type and if it comes from the err_vec-string, unite it with other possible errors from the caller and then return them. If the error from foo is something else, I want to return it directly.
This kind of code, however, won't compile because I need to provide some type annotations for downcast_ref() but I'm at a loss as to what that should be. Error types are still a bit mysterious to me.
So my question is basically: is this general approach ok and what type do I need to provide here? I tried String but that wasn't right. Any help is greatly appreciated.

Your example is pretty messy and I'm unsure what you are trying to do exactly, but in general:

  1. Unless you need to do something more fancy than just forwarding it when you get an error, you should use the question mark operator to propagate errors.
  2. If at all possible, avoid using downcast_ref to check for errors. If you need to check for a certain kind of error, you should avoid casting it to an Box<dyn Error> before inspecting the error.

As for the type annotations you need to provide to downcast_ref, well that depends on what kind of error you are trying to check for.

1 Like
  1. This is what I did throughout the rest of my code so far
  2. Any specific reason why I should avoid downcast_ref? Maybe I should restructure my functions but that's basically the reason I was asking the first place: I'm trying to learn how to handle errors better. Sorry if my description was confusing.
    Basically what I have is this: a function (foo) has various method calls that return different kinds of errors. Since this is the case I specified the return error type as Box <dyn std::error::Error> because that let's me return different kinds of errors (only known at runtime).
    If foo returns a specific error type (the one that is returned with err_vec), I want the calling function to access the data inside and handle it further. If this is a bad idea, I was wondering how to do it better. Other than that, I don't know what error type return Err("some_string".into()) is when the return type is Result<foo, Box<dyn std::error::Error>>

I recommend avoiding downcast_ref because it is cumbersome and error-prone to look inside it. For the case where you need to return one of many errors types, the alternative is as follows:

#[derive(Debug)]
enum FooError {
    IOError(std::io::Error),
    Utf8Error(std::str::Utf8Error),
}

Here I just used two random errors from the standard library. Then to handle the error, the caller can match:

match foo() {
    Ok(x) => {
        // do stuff
    },
    Err(FooError::IOError(err)) => {
        // handle IO error
    },
    Err(other) => {
        // propagate other errors
        return Err(other);
    },,
}

There are various forms of traits you can implement on your error type to give it more functionality. These are optional, but I will go through them here:

To make the error printable with println!("{}", error) you can add this:

use std::fmt;

impl fmt::Display for FooError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Just forward the call to the inner type.
        match self {
            Self::IOError(io) => fmt::Display::fmt(io, f),
            Self::Utf8Error(utf8) => fmt::Display::fmt(utf8, f),
        }
    }
}

To make the error convertible into a Box<dyn Error>, you can add this. (this also requires the Display impl above)

impl std::error::Error for FooError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::IOError(io) => Some(io),
            Self::Utf8Error(utf8) => Some(utf8),
        }
    }
}

To make the question mark operator work on IO errors in methods that return a FooError, you can add this:

impl From<std::io::Error> for FooError {
    fn from(err: std::io::Error) -> FooError {
        FooError::IOError(err)
    }
}

and similarly for utf-8 errors:

impl From<std::str::Utf8Error> for FooError {
    fn from(err: std::str::Utf8Error) -> FooError {
        FooError::Utf8Error(err)
    }
}

The thiserror crate can automate the generation of all of these impl blocks.

To be clear, all of this is only really relevant if you want to inspect the error later on. If you are just going to print it, don't bother and use Box<dyn Error> or a library like anyhow. (anyhow is like Box<dyn Error> but with extra utilities)

2 Likes

Okay, thanks for taking the time to write this up! I'll think about it some more and then see what makes most sense in my case but I'll mark this as solution.
I feel like learning proper error handling is really a handful but still worth it in the end.

1 Like

In your use case you were talking about a vector of errors, which I ignored in the above post. However to return multiple errors you can just use a Result<OkType, Vec<SomeError>>.

1 Like

Thanks, although in practise it was actually a vector that was then concatenated to a string