10 ms·
Overview of Rust error handling libraries
- monocasa 7y agoI really don't like this whole "screw the type system, just throw string errors that you don't know the type of" behind all of this. It feels like backporting golang's error handling to rust, which feels like a step backwards.
- tyri_kai_psomi 7y agoThat's a really unfair characterization of Go's error handling, and something only really novice Go developers do. https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully https://dave.cheney.net/2016/04/27/dont-just-check-errors-ha...
- monocasa 7y agoAnd pretty much every production golang codebase I've seen.
- weberc2 7y agoI've been using Go for 7 years and I don't think this is a mischaracterization at all. I definitely don't think it's something "only really novice Go developers do"--I see it all the time, including in popular open source libraries and even the standard library. I don't think it's contentious that Go's error handling needs some improvement (by which I mean "more standard, structured error handling" and not "syntax support for eliminating error handling boilerplate"--the latter is more contentious as far as I'm aware).
- TheDong 7y agoEvery time you use `fmt.Errorf` and `if err != nil { return nil, err }`, you're returning a stringly typed error up the stack. 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.
- skybrian 7y ago"Stringly typed" means literally a string. Go doesn't always distinguish between errors, but the error type does distinguish errors from non-errors and that's important. It seems better to have coarse-grained types than to standardize the wrong type hierarchy?
- pjmlp 7y agoEven some std packages do it.
- tyri_kai_psomi 7y agoThe std lib is not the paragon of go virtue as some people make it out to be. The maintainers have been frank with some of the mistakes and things they'd do differently now but because they place the Go 1 promise above everything else (a good thing), it has some patterns not many would recommend these days, error handling among them.
- monocasa 7y agoAnd kubernetes, docker, golang.com/x/*, etc. 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.
- cyphar 7y agoAlmost every large Go codebase does this with very few exceptions. Until the advent of github.com/pkg/errors there wasn't an easy way to uniformly implement error chaining so people would just prepend strings (if they have any context at all) and used fmt.Errorf religiously. These days, fmt.Errorf is still used but now you can in principle root-cause errors with errors.Cause -- unfortunately many Go libraries do not use github.com/pkg/errors (because of the nested vendor problem that existed for many years) and so you are stuck if you want to root-cause an error in an older library. 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.
- epage 7y agoSome factors for this - 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.
- monocasa 7y agoFor sure. 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.
- epage 7y ago> 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.
- monocasa 7y agoBy dumping all those together, you've lost context as to why or where these errors have come from. Just yolo displaying io errors is a terrible experience for your users.
- cytzol 7y agoThis is a nice overview of Rust's available error-handling crates, and some improvements to the language that are error-related. But I'd like to quickly point out that you do not have to pick a library from this list! I don't want anybody to read this article and go away thinking "I need to learn nine libraries in order to use Rust effectively". In the majority of cases, I've found what the language and std offer is enough for me. (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.)
- pornel 7y agoYeah, it does seem overwhelming from the outside. But all these libraries are just variations of boilerplate needed for the `std::error::Error` trait. 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.
- asdkhadsj 7y ago> 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. 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.
- deleted 7y ago[deleted]
- timw4mail 7y agoWhile dealing with errors in any langauge is irritating, I think Rust has the best ideas. * 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..
- shepmaster 7y ago> 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.
- thenewwazoo 7y agoI've never quite understood all the interest in these error-handling libraries. I've had great success even in large projects by impl'ing From when I need implicit conversion between error types. Most of the time, though, I want to actually handle the error at the calling site. I don't "bubble up" errors because they mean different things at different levels of the stack.
- oconnor663 7y agoI think the biggest benefit of these is automatic backtraces. That's what I tend to miss from e.g. Python. But this is usually more of a concern for applications than for libraries. If my application spits out "IO error: nonexistent file", I need to know which line it came from to know which file it's talking about. But in a library it's usually more obvious.
- monocasa 7y agoI mean, don't just bubble out errors until they lack context then. Rewrap them to add more context like ConfFileReadError(io::Error).
- thenewwazoo 7y agoI wholeheartedly agree with this sentiment. Another alternative, at the cost of some log noise, is to emit a meaningful log message at every call site that has to handle the error condition.
- shepmaster 7y agoThat would be a major point of my library, SNAFU, discussed in the article. https://docs.rs/snafu/0.6.0/snafu/ https://docs.rs/snafu/0.6.0/snafu/
- shepmaster 7y agoThe problem I identify with this is that the error creator needs to capture the backtrace for it to be useful. By the time the error has reached the application, the innermost parts of the stack have been popped and a backtrace is of limited usefulness. 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.
- swfsql 7y agostruct Error { XAtLine47, XAtLine88, YAtLine13, } This way you have error AND error place as contextualization.
- NieDzejkob 7y agoTo be honest, I can't tell if you're being serious.
- shepmaster 7y agoLike the sibling, I'm not sure if you are serious, but my library SNAFU (https://docs.rs/snafu/0.6.0/snafu/ https://docs.rs/snafu/0.6.0/snafu/) is similar to this at a step or two higher level. The idea is that you wrap lower-level errors (a file system error) with a higher-level context (opening the config file). 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.
- epage 7y agoRust brings some great features to the table but the combination of them opens up a new level of challenges to deal with which is why there is so much talk in the community on error handling libraries. 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.
- couchand 7y ago> 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. 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?
- jedisct1 7y agoI like the idea of TypeScript-like error handling. 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 :)
- epage 7y agoIs it employing anonymous enums? They come up for helping errors in the Rust community from time to time. The main dropback I see on relying on them is that they make it easy to accidentally expose your implementation details as part of your public API, making it a breaking change to upgrade or change a dependency.
- The_rationalist 7y agoIn typescript you can return union types. Rust doesn't allow that, at least not ergonomically.
- wallacoloo 7y agoOne error handling pattern I’d like to figure out how to implement in Rust is where you trace the error out from the point it was created rather than capturing a stack trace at creation time. Tracing the error this way is impactful when your application spans multiple processes or machines. Client/server systems, for sure (you can know what the server did to error, without extra noise in the trace from below where it received the command, and you also get a trace of the client’s side. It’s substantially more useful when the workload is distributed more. 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.
- shepmaster 7y agoCould you provide a deeper example of the difference you mean? Additionally, have you seen tracing (https://docs.rs/tracing/0.1.10/tracing/ https://docs.rs/tracing/0.1.10/tracing/)?
- AndyKelley 7y agoYou can also make it never inline, and use the return address instead of program counter. That's what zig does to implement this feature ("error return traces").
- wallacoloo 7y agoThis would mean an extra function call for _every _ use of `?` (rather than a conditional jump that the branch predictor has been well-primed for), even in the non-error path. My Rust code for some projects use `?` pretty frequently, to the point that I’d be concerned about perf. Of course, the only way to be sure is to profile. Do you have any perf measurements with this technique in zig?
- AndyKelley 7y agoBy default this feature is enabled in the Debug build mode only, so its perf impact isn't a concern in release builds. But that being said, the function call only happens in the error path, when returning an error from a function, right? Are you concerned about the perf of the error path? Maybe I'm not understanding something about your implementation. But yeah the way to be sure is to profile. When I get around to it I'll add a flag to allow enabling this feature in release builds and do some profiling.
- loeg 7y agoAs a Rust newbie, this is why I end up just using .unwrap() everywhere. result::Result, io::Result, is something a Some/None or an Ok/Error, etc, etc. What (non-string) errors does this function return, etc? I find it easier to check for and propagate integer error numbers correctly in C.
- unlinked_dll 7y agoOne pattern is to use your own crate-level Error type and then Result::map_err to bubble up other error types into your own thing. Example: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=48252c0056e0a13829bb5d24171687ed 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.
- shepmaster 7y ago> 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.
- pornel 7y agoUse `Box<dyn Error>` as your error type, and you won't need to know about any other error type. They all convert automatically and propagate through the `?` operator. failure and anyhow crates are `Box<dyn Error>` on steroids.
- skybrian 7y agoI'm wondering if there's any equivalent in Rust to how Zig does error traces? https://ziglang.org/documentation/master/#Error-Return-Traces https://ziglang.org/documentation/master/#Error-Return-Trace...
- mikedilger 7y agoI like file and line numbers with my errors: // 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 } }; }
- epage 7y agoSome of the highlighted error crates will include a full backtrace if you are using nightly rust. Not sure when it will hit stable.
- shepmaster 7y agoSome also produce backtraces on today's stable Rust and previous versions as well. SNAFU can produce backtraces back to stable Rust 1.31.
- Gibbon1 7y agoI've been doing that sort of thing in firmware for a long time. Record the file, line number and the caller and restart. 90% of the time it's obvious what the problem is by inspection.
- bjoli 7y agoIt bugs me to no end that Rust _almost_ got conditions and restarts. Apart from proper macros, conditions and restarts are one of the few things that CL has that no other language has mimicked. Sure, I have seen it faked in scheme with call/cc, but that will never be a proper, fast solution.
- mst 7y agoI think guile at least got prompt and abort functionality which makes it mostly doable? Also, bizarrely, http://p3rl.org/Worlogog::Incident http://p3rl.org/Worlogog::Incident exists.
- bjoli 7y agoNot only doable, but rather efficient. Delimited continuations are superior to undelimited ones in every way.
- gpm 7y agoWhere can I read about conditions and restarts?
- staticassertion 7y agoBox<dyn Error> for life
- unlinked_dll 7y agowhen your error handler can also fail
- staticassertion 7y agoThe top level error handler is inner_main().expect("it should work")