Item 3 described how to use the transformations that the standard library provides for the and types to allow concise, idiomatic handling of result types using the operator. It stopped short of discussing how best to handle the variety of different error types that arise as the second type argument of a ; that's the subject of this Item.
This is only really relevant when there are a variety of different error types in play; if all of the different errors that a function encounters are already of the same type, it can just return that type. When there are errors of different types, there's a decision to be made about whether the sub-error type information should be preserved.
It's always good to understand what the standard traits (Item 5) involve, and the relevant trait here is . The type parameter for a doesn't have to be a type that implements , but it's a common convention that allows wrappers to express appropriate trait bounds – so prefer to implement for your error types. However, if you're writing code for a environment (Item 33), this recommendation doesn't apply – the trait is implemented in , not , and so is not available.
The first thing to notice is that the only hard requirement for types is the trait bounds: any type that implements also has to implement both:
- the trait, meaning that it can be ed with , and
- the trait, meaning that it can be ed with .
In other words, it should be possible to display types to both the user and the programmer.
The only1 method in the trait is , which allows an type to expose an inner, nested error. This method is optional – it comes with a default implementation (Item 13) returning , indicating that inner error information isn't available.
If nested error information isn't needed, then an implementation of the type need not be much more than a – one rare occasion where a "stringly-typed" variable might be appropriate. It does need to be a little more than a though; while it's possible to use as the type parameter:
a doesn't implement , which we'd prefer so that other areas of code can deal in s. It's not possible to for , because neither the trait nor the type belong to us (the so-called orphan rule):
A type alias doesn't help either, because it doesn't create a new type and so doesn't change the error message.
As usual, the compiler error message gives a hint of how to solve the problem. Defining a tuple struct that wraps the type (the "newtype pattern", Item 7) allows the trait to be implemented, provided that and are implemented too:
For convenience, it may make sense to implement the trait to allow string values to be easily converted into instances (Item 6):
When it encounters the question mark operator (), the compiler will automatically apply any relevant trait implementations that are needed to reach the destination error return type. This allows further minimization:
For the error path here:
- returns an error of type .
- converts this to a , using the implementation of .
- makes the compiler look for and use a implementation that can take it from to .
The alternative scenario is where the content of nested errors is important enough that it should be preserved and made available to the caller.
Consider a library function that attempts to return the first line of a file as a string, as long as it is not too long. A moment's thought reveals (at least) three distinct types of failure that could occur:
- The file might not exist, or might be inaccessible for reading.
- The file might contain data that isn't valid UTF-8, and so can't be converted into a .
- The file might have a first line that is too long.
In line with Item 1, you can and should use the type system to express and encompass all of these possibilities as an :
The definition includes a , but to satisfy the trait a implementation is also needed.
It also makes sense to override the default implementation for easy access to nested errors.
This allows the error handling to be concise while still preserving all of the type information across different classes of error:
It's also a good idea to implement the trait for all of the sub-error types (Item 6):
This prevents library users from suffering under the orphan rules themselves: they aren't allowed to implement on , because both the trait and the struct are external to them.
Better still, implementing allows for even more concision, because the question mark operator will automatically perform any necessary conversions:
The first approach to nested errors threw away all of the sub-error detail, just preserving some string output (). The second approach preserved the full type information for all possible sub-errors, but required a full enumeration of all possible types of sub-error.
This raises the question: is there a half-way house between these two approaches, preserving sub-error information without needing to manually include every possible error type?
Encoding the sub-error information as a trait object avoids the need for an variant for every possibility, but erases the details of the specific underlying error types. The receiver of such an object would have access to the methods of the trait – , and in turn – but wouldn't know the original static type of the sub-error.
It turns out that this is possible, but it's surprisingly subtle. Part of the difficulty comes from the constraints on trait objects (Item 12), but Rust's coherence rules also come into play, which (roughly) say that there can be at most one implementation of a trait for a type.
A putative would naively be expected to both implement the trait, and also to implement the trait to allow sub-errors to be easily wrapped. That means that a can be created an inner , as implements , and that clashes with the blanket reflexive implementation of :
David Tolnay's is a crate that has already solved these problems, and which adds other helpful features (such as stack traces) besides. As a result, it is rapidly becoming the standard recommendation for error handling – a recommendation seconded here: consider using the crate for error handling in applications.
Libraries versus Applications
The final advice of the previous section included the qualification "…for error handling in applications". That's because there's often a distinction between code that's written for re-use in a library, and code that forms a top-level application2.
Code that's written for a library can't predict the environment in which the code is used, so it's preferable to emit concrete, detailed error information, and leave the caller to figure out how to use that information. This leans towards the -style nested errors described previously (and also avoids a dependency on in the public API of the library, cf. Item 24).
However, application code typically needs to concentrate more on how to present errors to the user. It also potentially has to cope with all of the different error types emitted by all of the libraries that are present in its dependency graph (Item 25). As such, a more dynamic error type (such as ) makes error handling simpler and more consistent across the application.
This item has covered a lot of ground, so a summary is in order:
- The standard trait requires little of you, so prefer to implement it for your error types.
- When dealing with heterogeneous underlying error types, decide whether preserving those types is needed.
- If not, use to wrap sub-errors.
- If they are needed, encode them in an and provide conversions.
- Consider using the crate for convenient, idiomatic error handling.
It's your decision, but whatever you decide, encode it in the type system (Item 1).
1: Or at least the only non-deprecated, stable method.
2: This section is inspired by Nick Groenen's "Rust: Structuring and handling errors in 2020" article.