Error handling for dummies

Hi folks, I'm a recent beginner, trying to get a hang of errors in an application I'm working on. Let's say I want to implement a method on a custom type, and I want the method just to update some of the type instance's fields and not return anything. Inside the method I call some functions from an external crate or two that return Results, which means I can handle the errors in my method or pass them out of my method by returning a Result, using ? for example. As in:

impl MyStruct {
    fn update_field(&mut self) {
        self.field1 = external_fn1(...).handle_the_error();
        self.field2 = external_fn2(...).handle_other_error();
    }
}

or

impl MyStruct {
    fn update_fields(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        self.field1 = external_fn1(...)?;
        self.field2 = external_fn2(...)?;
        return Ok(());
    }
}

My question is: is there a reason to use one approach or the other (or a different one altogether)? The second seems not preferred because it funnels different error types into the same Box, plus update's signature is more complicated. I could use match, but that gets ugly if there's a chain of methods returning Result. I'd prefer to use the first approach, but I don't know what I should use to handle_the_error. unwrap_or_else? Can I use that if I just want to print or log the error? If so, I'm having trouble figuring out how -- any suggestions welcome.

1 Like

I would prefer the first case under any of these conditions:

  1. The result-returning calls are guaranteed to be infallible, so you can just unwrap them and disregard error handling altogether.
  2. You may be able to "recover" with something like unwrap_or_default or unwrap_or_else on the result without panicking. This is good when you can safely choose some alternative value for T in Result<T, E>.
  3. You just live with the fact that the method may panic and you document which specific cases can potentially panic.

The second case (possibly with a concrete error type) is preferred when you do not wish to panic but you have no realistic alternative. So the best solution is to return an error and let some other part of the call stack deal with it. They may choose to pick an alternative T or even its default value, or they may choose to panic, or they may choose to propagate the error further.

Technically you can, but I would personally leave logging to something higher in the call stack. Preferably at the top-level of the executable. Which can use something like the unstable error_iter feature or context/chaining with anyhow display representations. It's reasonable to add more context to the error, but I don't believe that "handling an error" necessarily means "logging the error for the user".

Take for instance the case where you have a non-trivial application which has tons of bespoke error handling like this. That sounds like a situation that would greatly benefit from the DRY principle. Propagating errors is essentially an application of that principle in practice because all of the "actual error handling" can happen in one place.

2 Likes

Heya, what is "pragmatic" usually depends on the scope of the code's usage:

  • If it's a small app for personal use, handle_the_error may well be "print some stuff and exit"

  • If it's a library intended to be consumed by many people, then the pattern I use is:

    1. Create a separate pub enum MyLibError { .. }.

    2. Create a variant for each error, e.g.

      pub enum MyLibError {
          Field1SetError {
              /// Underlying error.
              error: external_crate_1::Error
          },
          Field2SetError {
              error: external_crate_2::Error
          },
      }
      
    3. Either impl From<external_crate_1::Error> for MyLibError, which allows you to go external_fn1(...)? in the calling code, or

    4. external_fn1(...).map_err(|error| MyLibError::Field1SetError { error })?

The actual translation of the MyLibError enum to human readable message will be done as late as possible, so that:

  • if the calling code is a web service, maybe it serializes that error, rather then sending a string.
  • if the calling code is a command line interface, it can turn it into a human readable message

I tend to avoid boxing if the consumer of the function needs to know the underlying error variant to handle each thing properly. But boxing is okay if they don't have to

2 Likes