Results are a kind of sum type, an umbrella under algebraic data types. They are states, which is the sum of the cardinality of their Ok and Error variants. For example:

enum Result<bool, ()> {
    Ok(bool)
    Err(())
}

is a 3-sum type, as there are 2 possible states for it’s okay variant, and one for the Error variant.

This is isomorphic (same “shape” / properties) to Option<bool>.

Option vs Result

Result<T, E> can be thought of as a more generalized version of Option<T>, when |E| has a cardinality of 1.1

Thinking of them as the same helps to simplify the mental model—on a high-level, they are one and the same.

Details about the ? Operator

Haskell has a do statement that maps the “successful” branch of fallible computations.

Rust has the ? which achieves this same feature in Haskell, but is more powerful because it’s an expression as opposed to a statement. This means that it can be used in along with or nested in other expressions.

It is somewhat equivalent to:

match operand {
	Ok(x) => x,
	Err(e) => return Err(From::from(e)),
}

The ? operator on a Result<T, E> type, is of type T. This is because the error branch diverges from the main code. In fact, the error branch has the [[The Never ! Type|type !]] (pronounced “never”), and is a Bottom type in Rust.

Using Option or Result in Practice

Single Failure Mode

For situations where a code can fail in only one way, it’s your choice on whether you want to return Option or Result. A reddit post (shown in the video) seems to suggests that the official docs suggests to use Result type in such situations, even though some official libraries don’t practice it either.

The way the author thinks about it is that if there’s only one obvious failure mode, then Option suffices.

Examples include retrieving results from a dictionary, where the only failure mode that’s intuitive to the API caller is equivalent to KeyError in Python.

The benefit of using Option is that it keeps the API cleaner, instead of Result<T, E> where the Error type has to also be specified.

Multiple Failure Modes

When there’s multiple failure modes, it’s likely better to use Result as it can provide more information to the caller.

#[derive(Debug)]
enum GrandparentError {
    NoParent,
    NoGrandparent
}
 
struct Node { }
 
impl Node {
    fn parent(&self) -> Option<&Node> {
        todo!()
    }
}
 
fn grandparent(n: &Node) -> Result<&Node, GrandparentError> {
	Ok(
		n.parent().ok_or(GrandparentError::NoParent)?
		 .parent().ok_or(GrandparentError::NoGrandparent)?
	)
}

The caller can then decide how they want to handle the error in their code, or not at all and simply panic or bundle up their error types. However, if we as the implementor do not provide it, then there’s no way at all for the caller to distinguish between failure types, which makes for a poor API.

Bubbling Up Multiple Distinct Failure Types

Libraries

[[thiserror]] vs [[anyhow]]

The Infallible Enum

Sometimes, you may be writing code that can never fail, for a trait that expects an Error trait. In such a case, using () works but is semantically incorrect.

This is because () implies that there is one failure state, when in reality there are no failure states.

For this, we can use

pub enum Infallible {}

for which there are exactly zero states, to imply that it cannot ever fail.

Going back to the previous example, Result<bool, Infallible> removes the Result type, effectively making it isomorphic to bool.

Footnotes

  1. A more recent reading from Rust for Rustaceans points to this not being semantically equal. Result contains the usage of #[must_use]. ↩