Overview of Rust error handling libraries
blog.yoshuawuyts.com
blog.yoshuawuyts.com
(In fact, it leaves my favourite solution out: using the derive_more crate to auto-generate From implementations for the ? operator to use. From is a general trait, useful for far more than error handling, which is probably why it's omitted here.)
And the variety exists, because there are different needs and trade-offs:
• You may want an error type that takes 0 bytes, so that `Result` is cheap for trivial high-performance functions (e.g. parsing libraries that may fail on every byte).
• You may want your error type specify exact cause of failure in your library in a programmatic way, so that callers can turn it into a precise, localized error message for the end user.
• You may want to have a super rich detailed error object with all the context, e.g. an entire HTTP response for a 5xx status.
• And some application developers prefer having an "I don't care, just give me the backtrace" error wrapper for everything, to speed up the development and not spend time on micro-managing error types.
All of this works nicely and uniformly in `Result<Ok, Err>` with some `Err` type written (or generated) by you.
That's me tbh. I actually love Rust's error story atm, I always use an Error enum and I have made it do everything I want.. except one thing: Low (no?) cost backtraces.
My error enums give me all the API-level specifics I want, but often I make the variants grouped by general code area. It's guided by my program flow, not debug introspection. So if I don't need to branch a codepath from an error object, it typically just goes into a single variant and I shove some message in there.
What I'd love is to be able to easily and quickly just bake backtraces into this "non-program flow defining type". Bonus points if it could be done at compile time.
I've thought about using a macro at error creation time to make it a compile-time poor mans backtrace, but I'm unsure if just one level of stack trace is all that beneficial.
Either way I know I want something that's no more costly than:
foreign_err.map(|foreign_err| MyError::Dump(format!("some description: {}", foreign_err)))
edit: I should add, internally my perspective is coming from Go, so wrapping repeatedly in strings is fairly welcome to me. Just adding some low-cost default context to this would be a huge help.Crates like my SNAFU (https://docs.rs/snafu/0.6.0/snafu/) help abstract this away. Whenever the standard library implementation stabilizes, you will likely see many more errors including backtraces.
I fear bubbling an error up and repeatedly adding a stack trace or some junk like that. Admittedly I have not looked too much into Backtrace, but I hope my concern makes sense.
SNAFU addresses this by allowing `Option<Backtrace>` in the error struct. The backtrace is only captured when an environment variable is set by the end user.
Checks the username of who you responded too: I'll blindly bet yes to that question for any amount you desire.
People are using rust as they do other general purpose programming languages that have somewhat of a focus on systems programming: to write code. Your question isn't really easy to answer in the same way that someone asking "what are people building with python?" isn't useful. The answer is everything from webpages to text editors to operating systems, and everything in between.
I use it for all my personal projects as well. Which are basically the same types of things I wrote in Go. Though, I also am experimenting with WASM DOM UIs, something I've not yet used in Go.
I have a feeling that inside of 5 years Rust will be blowing up on the job market. It's just so good and so practical that once you pick it up, anything less well designed will feel like a kludge and hack that might blow up on you at any point.
My current pet project in Rust is writing a functional language compiler that compiles down to System F/F omega and eventually I'm going to build some kind of virtual machine
E.g. I have written a cache-warmer that preopens and does readaheads (posix_fadvise) for assets from NFS and offers the already open file descriptors to the calling process. The NFS share offers decent throughput but terrible latency (compared to NVMe). Opening files is slow, walking directories sequentially is slow. And then hitting cold pages is slow. Doing all this in rust and handing the file descriptors over to node lets me reduce minutes to seconds. But if someone doesn't like it, there's a flag that turns it all off, back to the slow path.
Another use is essentially replacing shell-scripting when some REST APIs, JSON parsing and so on are involved. E.g. to glue together some status reporting in CI. There are so many different unreliable APIs without client libs involved that I appreciate the error handling.
I also have a lot of personal pet projects written in rust: an MMORPG-style game, accounting software, various WASM widgets, some OS fiddling, some work extending servo as a browser that implements umatrix-like control but lets you set options per domain (e.g. 3rd party cookie, user agent setting, etc, all per-domain).
I've also contributed a number of small libraries that I wrote for my projects to open source (textnonce, float-cmp, mailstrom, formdata, mime-multipart, solvent, ddsfile, pemmican, resolv-rs, email-format, and others).
It's usually not much more code than I would have written with an error handling library and it's fully transparent, understandable for anyone with a rudimentary understanding of Rust. And I get to have one less dependency.
It's certainly less transparent, but I prefer this method of error handling, and writing out the Impls manually can get out of hand (as well as having to adding new fields to the enum).
This is part of the reason that SNAFU tries to make it lightweight for library authors to add backtraces and allows application developers to turn them on.
Last time I needed this, I created a simple ErrorWithPath<E> wrapper around Error which also stores the path as a String, and a helper function which calls a closure and wraps any errors on its result with that wrapper. (If you want to take a look, it's at https://github.com/cesarb/filestatrec/blob/master/src/error....)
I had a major aha moment recently when reading something that described errors as just types. When put that way it suddenly made so much more sense to me.
// No need to use this directly. See the e!() macro.
#[derive(Debug)]
pub struct ErrInfo {
pub file: &'static str,
pub line: u32,
pub msg: &'static str
}
impl std::fmt::Display for ErrInfo {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if cfg!(debug_assertions) {
write!(f, "[{}:{}] {}", self.file, self.line, self.msg)
} else {
write!(f, "{}", self.msg)
}
}
}
/// Use this with `failure` crate's `ResultExt` like this
///
/// ```
/// $r = function().context(e!("Function failed"))?;
/// ```
///
/// Error context will then include that message, and the file and line number.
#[macro_export]
macro_rules! e {
($msg:expr) => {
$crate::ErrInfo {
file: file!(),
line: line!(),
msg: $msg
}
};
}Some of the competing needs that need to be dealt with to understand why include
- Quickly create errors and extend them for prototyping while scaling to programmatically reacting to them
- Easily convert errors with `?` has drawbacks. `?` wants you to implement the `From` explicit conversion trait but that becomes part of your public API, exposing implementation details (switch dependencies or one bumps major is technically a breaking change for you). Another problem is you can't both implement the `Error` trait and support `From` from anything that implements the `Error` trait because it will conflict with Rust's blanket implementation of `From` converting yourself into yourself. We need specialization to solve this.
- You can return `Result<T, E>` from main but it prints `Debug` information rather than user-friendly `Display`. So we need to deal with `Debug` printing the right thing most of the time while printing something user-friendly in `main`.
- Rust users have their errors wrap underlying errors in a giant chain. Rendering of this chain needs to be decoupled from a single `Error`'s `Display`.
- We aren't clean which errors in a chain are context and which are meant to be actual root causes to programmatically act on
- Supporting `no_std` for embedded cases.
On top of all of that are unsolved problems like process exit codes specialized for the error being returned, debug-only information (e.g. opt-in to rendering an original `errno` or `HRESULT`), error cloning (important for caching failable-operations), error serialization (important for network proxying APIs).
I think some in the Rust community are seeing Yoshua's comaprison and feeling like we are converging on a solution. While I think that is true for `derive(Error)` (code-genned trait implementations), I don't think this is true for our patterns for how to structure and communicate content in errors or how to do Dynamic errors. This post made me feel like Rust is in a rut. People have taken a pattern or two from cargo's source and refined it without branching out more into other ways of solving the problems. This ends up leaving a lot of the problems i listed above unsolved or less than ideal.
I'm toying with ideas for an alternative approach for solving most of these problems; just need to get some more time to get it in a shareable state.
I believe this issue would only come up if you're trying:
impl<T: std::error::Error> From<T> for MyError
which seems like a really clumsy way to handle it. In most real cases, wouldn't you write the From impls for the specific types of errors you're expecting?In an application, yes. The problem is in libraries because the `From`s` are public so you have put the error type for your dependency in your public API and if you change dependencies or upgrade past a major, you've broken your API. Granted, it is unlikely someone is going to use it ... but I wouldn't put it past people to take shortcuts like that.
The best I’ve come up with is using a custom ResultExt class which implements std::op::Try, marks its trait methods as inline(always), and then captures the program counter whenever you use `?` on it in order to build a trace. It’s not the worst, but it still feels a bit odd — particularly the forced inlining since that will surely break if you’re working with std::ops::Try from behind a trait object.
Additionally, have you seen tracing (https://docs.rs/tracing/0.1.10/tracing/)?
std::ops::Try looks like: - fn into_result(self) -> Result<Self::Ok, Self::Error>; // called on `x` whenever you use the `x?` syntax. - fn from_error(v: Self::Error) -> Self; - fn from_ok(v: Self::Ok) -> Self;
It's been a while since I explored this. I was thinking you'd `#[inline(always)]` the `into_result` method, but now that I look at the trait again, I think you would only force inline the `from_error` method and it should still work but without impacting the non-error path much.
https://dave.cheney.net/2016/04/27/dont-just-check-errors-ha...
The entire stdlib does this. An error stack is never present from stdlib functions' errors, and third party libraries I see it just as infrequently.
If only novice go devs do that, than the entire go core team are novices, as well as the authors of popular go projects like kubernetes and docker.
It seems better to have coarse-grained types than to standardize the wrong type hierarchy?
And it's not like they couldn't add better types to errors without breaking backwards compat. Returning a type that implements error instead of fmt.Errorf doesn't break existing semantics.
All of that being said, modern Go (written in the past few years) could avoid these pitfalls. But very few production codebases were both written in the past few years and only use libraries written in the past few years.
I've worked on Go codebases for the past 5-6 years.
- When prototyping, deferring worrying about exact error types is helpful
- In applications, you aggregate so many types of errors, sometimes it is easier just to dynamically accept any.
1 - Just have a Todo(String) in your error enum while prototyping. Remove it as you clean up a prototype, and you get a nice set of compiler errors telling you everywhere that needs to change.
2 - If you're aggregating so many error types that it's unwieldy, you're probably doing to much in one place. That has all sorts of down stream issues with testability and what not. Hacking your error types to make it easier is probably not the best option.
I'd disagree. In a static-site-generator, I need to deal with
- file format errors
- syntax highlighting errors
- io errors
- Template language errors
- Adhoc errors I construct throughout the application
- Regex errors
- RSS errors
- jsonfeed errors
- sitemap errors
- git errors
Without even getting into any errors from the webserver that is being run. Yes, I could carefully construct an error enum for each subsection of the application and bubble those up ... but why? I'm not programmatically acting on these but sending them up to the user.
* Pattern-matching for success/error check
* Error passing with the `?` operator
* The correct error seems to appear when panicing due to an `unwrap` call
The most irksome thing remaining is to have to create your own error objects..
That's generally the purpose of the libraries outlined in the article — reducing the pain of creating the custom error types.
Sure, I have seen it faked in scheme with call/cc, but that will never be a proper, fast solution.
Also, bizarrely, http://p3rl.org/Worlogog::Incident exists.
The book is available online I think.
That is definitely an interesting concept.
struct Error {
XAtLine47,
XAtLine88,
YAtLine13,
}
This way you have error AND error place as contextualization.Taken to the extreme, you have a unique context for every possible error source inside your library, which means you could directly search for the name to find the line number.
This does make me wonder how I could add line / file information to an error automatically though.
https://ziglang.org/documentation/master/#Error-Return-Trace...
Or more generally, functions that can return different types without having to define an `enums`. If only because naming these single-use `enums` is hard :)
https://play.rust-lang.org/?version=stable&mode=debug&editio...
This makes error handling a little more verbose but you can use some quick and dirty macros for that.
That is the point of most of the libraries described in the article — to prevent having to recreate this particular wheel each time you need it. Do all Rust programs/libraries need one of the libraries? Not at all, but I find that using one that has some nice bells and whistles will encourage the programmer to improve the errors and their messages.
failure and anyhow crates are `Box<dyn Error>` on steroids.
inner_main().expect("it should work")