When I first started writing Rust code, I went through several iterations of error handling. From that experience, I have these learnings:
- Embrace the
Result<R, E>type for general purpose error handling. Note that "stack frames" will be lost in this error handling unless you do something about it. - Embrace
panic!()(or that family of error handling) for asserting preconditions in your code or really any error that could only happen if the program was incorrect. The beauty of a panic is the you will get a backtrace if theRUST_BACKTRACEenvironment variable is set. - When you are uncertain, use
Result<R, E>, as that can easily be converted into apanic!()by the caller.
The next natural question is what type to use as the E parameter of Result<R, E>.
When designing any error type, I consider these things:
- Make sure it is an "owned" type, there are rare exceptions to this rule, but working with non-
'staticerrors is difficult and should be avoided. - When you have a function
Athat only triggers an error when calling functionB, it is convenient to "pass the buck" and simply use?to propagate the exact same error. However, before blindly doing this ask yourself if there is critical contextual information which may be lost. For example, if you are calling functionBon every element of a collection, you may want to add contextual information about which element that error originated from. In this example, you may want to.map_err()to a new error type that attaches the relevant contextual information. - Consider what types of errors a caller may want to discern between. For example, if doing any sort of I/O operation, knowing that a network stream was interrupted vs invalid input being provided are typically going to be handled differently by callers since an interrupted network stream may be resolved with a retry.
- Keep the errors constrained to only the set of errors that are actually possible. Note that this flies in the face of many popular crates that define a single error type for their entire crate (an anti-pattern in my opinion). This does two things:
- If a new error variant is introduced or one is removed, you get compile failures. This is much better than getting unexpected behavior at runtime! If a variant is removed, the caller may also be able to reduce their error handling code.
- Users of your code have much better signal on what errors actually need to be handled vs errors that are simply possible because of a poorly designed error.
- Write error messages for humans and avoid collecting "stack traces". If you want an error with a stack trace, maybe you really want a
panic!()instead.
In order to make it easier to create my error types thiserror is enormously valuable in reducing the boilerplate code.
Top comments (0)