Error handling, Result, Error

I am very confused about the documentation and locations of Result and Error.
We have a lot of Result declarations in the different std modules.
There is also a Result<()>. What is that?
Can anybody give me a few clues about how to handle errors and which types to use?
I need a 'default' way to handle errors.

the simplest one...

fn check(&self, index: usize) -> Result??
{
    if index > 42
    {
        Error??
    }
    {
        Ok(())
    }
}

This is the Result<T, E>. It is re-exported various places, such as in std, but it is defined in core so that no_std libraries can also use it. (In general, things defined in core are re-exported in std.) T is the Ok payload type and E is the Err payload type.

Here's an example:

fn check(index: usize) -> Result<(), &'static str> {
    if index > 42 {
        Err("index is larger than 42")
    } else {
        Ok(())
    }
}

Ok and Err are the same as Result::Ok and Result::Err. They are used so often that they are imported into your namespace by default, just like Result itself is.

Despite this example, &'static str isn't really a great default for the error payload. anyhow may be a better place to start. (Good error design is a very large topic.)


There is also a convention where libraries or modules define their own aliases to Result, usually replacing one or both of the generic types, such as the E error payload type, with a specific type used throughout the library or module.

For example, std::io::Result<T> is an alias for Result<T, std::io::Error> and core::fmt::Result is an alias for Result<(), core::fmt::Error>. (As you can see, there is also a convention of naming your domain-specific error type Error.)

The Result<()> you refer to is one of these aliases.

When you need to disambiguate the different Result types/aliases, you can refer to them via their module instead of importing their names, or you can import them under different names.

use std::fmt;
struct S;
impl fmt::Display for S {
    //                                           vvvvvvvvvvv
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        Ok(())
    }
}
use std::fmt::{Display, Formatter, Result as FmtResult};
//                                 ^^^^^^^^^^^^^^^^^^^
struct S;
impl Display for S {
    //                                      vvvvvvvvv
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        Ok(())
    }
}
5 Likes

Thanks. Understood. Not sure (yet) if I like all these convention aliasses, but anyway.
I also implemented my own 'DecompressionError' in my program.
In C# you just raise an exception deep in the code and get the complete call stack in front of you, including line numbers.
Now if my decompression function a() fails because it calls function b() and function b failed, how can I know the exact details?
(low level function b is called all over the place).

If it's unrecoverable at the point of error, you panic.

If it's not and you still want backtrace-esque details, you have to add code to track context, either via some library like anyhow or with a lot of custom code.

1 Like

I have a bit of understanding now of Result and Option and how to define / handle them.

So can we basically say the following?

  • Any function where something can go wrong must return a Result<T, E> or Result<(), E>..
  • Any function_a that calls function_b should correctly handle the Result of function_b.

You can also return Option if it is obvious what the error case means, like in Vec<T>::get_first. You don't need a CannotGetFirstElementOfEmtpyVecError, because that tells you nothing more.

Which is often implemented using ? to just propagate the error, or sometimes result.map_err(...)?

Ok. Thanks. Yes I use Option too in obvious cases.
I somewhere saw this:

  • make it work (use unwrap)
  • make it correct (catch the mistakes)
  • make it fast
2 Likes