← Back to index
PR #2820Work-in-progress preview from an open pull request.View on GitHub ↗
REVIEW
#2820

box_error_alias

Authorrichard-uk1
CreatedNov 20 2019
UpdatedJul 31 2020
Rust Issue

Add a type alias to std::error of the form

pub type BoxError = Box<dyn Error + Send + Sync>;

which given the absence of a lifetime is also 'static.

Motivation

Rust error handling has a long and complicated history, and it is still evolving right now. This reflects the fact that errors perform a multitude of functions, and these different functions require different implementations: for example, if errors are regularly encountered in a real-time thread, then one would not want their creation to involve an allocation or the indirection of a trait object, whereas for a CLI app, most errors may not occur on the happy path, and so their performance is inconsequential. Likewise, a low-level library may want fine-grained control of error handling and recovery, whereas a CLI app may only want to handle a small subset of errors, simply printing a message to screen for the rest.

The error trait in Rust's standard library provides a helpful way to perform two tasks: firstly it requires a Display implementation - so all errors can be logged/written/etc, and secondly it provides the error with the option of exposing another inner error, thereby allowing chains of errors to be created. Research into the best way to represent errors is ongoing in Rust, and this RFC does not attempt to resolve the discussion around how error handling in Rust should evolve. Instead, it proposes a simple type alias to serve the following motivations

  1. A unified name for boxed errors, to make them easier to recognise,
  2. A short descriptive name for error trait objects, to reduce code noise and cognitive load,
  3. A place to show the value of making errors Send + Sync + 'static, and
  4. A place in the standard library to document the pattern of using a trait object to return any error, aiding discoverability.

Currently what happens in practice is Rust programmers either create an alias for Box<dyn Error + Send + Sync> in some utility module, where each user will use a sligtly different name, or just write out the full type that BoxFuture aliases.

This idea evolved on a thread in irlo before this RFC was written, and there names such as AnyError were suggested, as well as using an opaque object with a raw pointer as is the case in the failure crate. However, this RFC only satisfies the narrow goal of providing a trait alias for Box<Error + ..>, allowing more experimentation to take place regarding the specific design of an opaque and more full-featured error type in the crate ecosystem.

Guide-level explanation

Once you start doing anything more advanced than arithmetic with a computer, you will encounter routines of the computer that may fail. The source of these failures may be hardware-related (e.g. a resistor has shorted in your temperature sensor peripheral), a bug in some code you (or someone else) wrote, the misuse of an interface, or a network failure, amongst many others. Ideally we design programming languages and libraries so that programming mistakes are caught when the program is compiled rather than at run-time, but obviously this is not always possible. It is therefore necessary to have run-time error handling.

Not all errors are the same: sometimes an error can be corrected on the fly, sometimes an operation must be re-tried (e.g. a tcp packet failed to arrive), and sometimes the error represents something catastrophic and the best response is to terminate the program with a useful error message. These different errors require different types of data: an error that will be instantly corrected should be able to choose whether it is stored on the heap or the stack, whereas for an error that will terminate the program the performance impact of a trait object/allocation will be negligable. The functionality of errors may be further constrained by the requirements of the programming environment: an embedded chip with low memory performing tasks in realtime may not have run-time memory allocation as an option.

In general, Rust programmers will create different types to handle these different situations. The type for a recoverable error may just be some fixed-size block of data, whereas a more serious error may contain references to descriptions of the error or other errors that cause it. Rust provides the Error trait, which includes converting the error into a string and referencing any underlying error, but Rust programmers can still implement this trait even though they don't want to be allocating strings or inner errors, since it is the caller of the methods who causes the allocations/etc. In this way, Rust allows all errors to be able to describe themselves, which is very useful during development and prototyping, as well as helping to support different use cases for library code.

Whist the Error trait helps error authors to provide self-describing errors, it does not help code using these errors to easily combine them with other errors of different types, and displaying the contents of an error that might be one of multiple types. The standard way to solve this problem is to create a trait object behind a thick pointer, hence the signature Box<dyn Error..>. The trait object obscures the actual type, instead presenting the Error interface, and is easily created from any error using the impl From<'a, E: Error + Send + Sync + 'a> for Box<dyn Error + Send + Sync + 'a>.

BoxError is simply a synonym for Box<dyn Error + Send + Sync>. Here are some examples showing its usefulness.

Example: Simple database function.

Here we are getting a database version from a file and returning it. We are in the prototyping stage and don't want to devote too much time to complex error handling.

fn db_version() -> Result<usize, BoxError> {
    let version = fs::read_to_string("/path/to/version/file")?
        .parse()?;
    Ok(version)
}

Reference-level explanation

The implementation is very straight forward, simply the code in the summary.

Drawbacks

  1. Increases std api surface area, although semantically there is no change since it only a type alias is added.
  2. Could potentially encourage people to use it instead of a specialized type like anyhow::Error.
  • I would challenge this drawback with the argument that even after the addition of a hypothetical AnyError, there will still be times when a BoxError is appropriate for its simplicity. It may turn out that this satisfies 99% of use cases for type-erased errors, and any more specialized solutions can live in crates.

Rationale and alternatives

This RFC proposes a very small addition that provides a type alias that many people define manually. The only real alternative is to do nothing and maybe land a more specialized type-erased error type in the future.

The rationale for not including a specialized error type is that it is not clear what the best design for it would be, especially since the story around backtraces is not yet finalised. It's also not clear if a specialized type is necessary. The disadvantages of BoxError over a hypothetical AnyError are

  1. An increased memory footprint (increasing the size of Result in the case that the Ok variant is small), but with fewer memory indirections. If this is an issue, then a concrete error type is more appropriate.
  2. The Debug implementation defers to the inner error type. The hypothetical AnyError could use the inner error's Display type, leading to better messages in the fn main() -> Result<(), AnyError> case.

Prior art

There are many error models in the Rust ecosystem, including many type-erased error types. Crates include anyhow and failure among others. Prior art for a type synonym is the many crates that define it internally.

An example of prior art of name choice is the BoxFuture type in the futures crate.

Unresolved questions

  • Should the error be Send + Sync. It seems like a good default, and nothing is stopping Rust programmers simply not using the alias if they want a Box<dyn Error>.

Future possibilities

There are many ways that the Error trait could be enhanced, for example by providing a dedicated AnyError type, or providing a method like Error::wrap that would take an error, and make it the source of an AnyError. This RFC attempts to be very focused on a small change, and so these ideas are better discussed elsewhere.