Errors

Two types of errors:

Representing Errors

Do we enumerate all possible errors so that the caller can distinguish them or do we provide a single opaque error.

Enumeration

The user needs to be able to distinguish between different error cases so that they can respond accordingly. So we use an enum.

When making our own error types we should take care to:

  1. Error type should implement the std::error::Error trait, which provides callers with common methods for introspecting error types. e.g. Error::source method a mechanism to find the source of the underlying error. This is most commonly used to trace the error to the source cause.
  2. The type should implement both Display and Debug so that the caller can print meaningful messages. The rule of thumb is a line describing the error for the Display implementation and more descriptive error including information that can help in debugging for the Debug implementation.
  3. Wherever possible it should implement both Send and Sync, so that users can share the errors across thread boundaries.
  4. Wherever possible the error type should be 'static This allows the user to easily propagate the error up and down the stack without running in to lifetime issues. It also enable the error to be used more easily with type-erased error types.
pub enum FileCopyError {
    In(std::io::Error),
    Out(std::io::Error),
}

Playground example of error enumeration.

Opaque Errors

Sometime the application using your code can’t meaningfully recover from the error, even if knows exactly the source of the error. In cases like this we want to provide a single opaque error type.

The error should implement Send, Sync, Display, Debug, and Error (including the sources method). Internally we might want to represent more fine-grained details, but there is no need to expose those to the users of the library.

Exactly how opaque error type should be is mostly up to us. - it could be a type with all private fields that exposes only limited methods for displaying and introspecting the error - it could be a severely type-erased error type like Box<dyn Error + Send + Sync + 'staic>, which reveals nothing more than the fact that it is an error and does not generally allow introspection.

Using Box<dyn Error> leaves users with little choice but to bubble up the error.

Type-erased errors often compose nicely and allow expression of an open-ended errors. We can easily combine errors from different sources without having to introduce additional error types.

e.g.  If we write a function whose return is Box<dyn Error + ...> the we can use ? across different errors types inside that function, on all sorts of errors and they can all be turned into a common error type.

’static bound in Box<dyn Error ..> allows

  • the propagation of errors without worrying about the lifetime bounds of that method that failed
  • access to downcasting

Downcasting

Downcasting is the process of taking an item of one type and casting it to a more specific type.

In the context of errors, downcasting allows a user to tuen a dyn Error into a concrete underlying error type when the dyn Error was originally of that type.

The downcast_ref method returns an Option which tells the user whether or not the downcast succeed. This method only if the argument is 'static. If we return an opaque Error that is not 'static we take away the user’s ability to do this kind of error introspection should they wish.

See docs for trait Any

Propagating Errors

Rust’s ? operator acts as a shorthand for unwrap of return early when working with Errors.

?:

  • performs type conversion through the From trait
  • in a function that returns Result<T, E> we can use ? on any Result<T, X> where E: From<X>.

This feature makes error erasure through Box<dyn Error> so appealing. We can just use ? everywhere without having to worry about the particular error type.

NOTE: The ? operator is just syntax sugar for a trait tentatively called Try. See: https://doc.rust-lang.org/std/ops/trait.Try.html

See playground example