31 ms·
The one thing that sold me on Rust (going from C++) was that there is a single way errors are propagated: the Result type. No need to bother with exceptions, fu
by dvratil 1y ago
The one thing that sold me on Rust (going from C++) was that there is a single way errors are propagated: the Result type. No need to bother with exceptions, functions returning bool, functions returning 0 on success, functions returning 0 on error, functions returning -1 on error, functions returning negative errno on error, functions taking optional pointer to bool to indicate error (optionally), functions taking reference to std::error_code to set an error (and having an overload with the same name that throws an exception on error if you forget to pass the std::error_code)...I understand there's 30 years of history, but it still is annoying, that even the standard library is not consistent (or striving for consistency).
Then you top it on with `?` shortcut and the functional interface of Result and suddenly error handling becomes fun and easy to deal with, rather than just "return false" with a "TODO: figure out error handling".
- jasonjmcghee 1y agounfortunately it's not so simple. that's the convention. depending on the library you're using it might be a special type of Error, or special type of Result, something needs to be transformed, `?` might not work in that case (unless you transform/map it), etc. I like rust, but its not as clean in practice, as you describe
- koakuma-chan 1y agoYou can use anyhow::Result, and the ? will work for any Error.
- deleted 1y ago[deleted]
- deleted 1y ago[deleted]
- ryandv 1y agoThere are patterns to address it such as creating your own Result type alias with the error type parameter (E) fixed to an error type you own: type Result<T> = result::Result<T, MyError>; #[derive(Debug)] enum MyError { IOError(String) // ... } Your owned (i.e. not third-party) Error type is a sum type of error types that might be thrown by other libraries, with a newtype wrapper (`IOError`) on top. Then implement the `From` trait to map errors from third-party libraries to your own custom Error space: impl From<io::Error> for MyError { fn from(e: io::Error) -> MyError { MyError::IOError(e.to_string()) } } Now you can convert any result into a single type that you control by transforming the errors: return sender .write_all(msg.as_bytes()) .map_err(|e| e.into()); There is a little boilerplate and mapping between error spaces that is required but I don't find it that onerous.
- johnisgood 1y agoI scratch my head when people try to justify Rust's implementation of X. It looks absolutely horrendous, IMO. I would rather have what OCaml has: https://ocaml.org/docs/error-handling https://ocaml.org/docs/error-handling.
- Cloudef 1y agoYou can use anyhow, but yeah zig generally does errors better IMO
- ziml77 1y agoErrors are where I find zig severely lacking. They can't carry context. Like if you're parsing a JSON file and it fails, you can know that it failed but not where it failed within the file. Their solution in the standard library for cases like this was to handle printing to stderr internally, but that is incredibly hacky.
- Cloudef 1y agoThe std has diagnostics struct you can give to the json parser. Zig is manually memory managed language so it doesnt have payloads in errors for a good reason.
- ziml77 1y agoManual memory management is not a reason Zig couldn't have supported sum types for error returns. You don't need an allocator involved at all to return a discriminated union like Rust uses. I actually find Zig quite a pleasant language other than my gripe with not handling more complex errors as cleanly.
- Cloudef 1y agoYou would have to give up on global error set with sum types, unless you want to see the global error set bloat with the largest sum type. Also if the error payload needs allocating what are you gonna do? Allocating is no no no here. I know rust will just panic in OOM but zig chooses not to do that. Even if you allocated, since zig has no borrow checker the error handling becomes nightmare as you now have to deal with freeing the potential allocations. Using out parameters for context, or diagnostic pattern like std does is not bad at all imo. The only nice thing error payloads give you is better error messages in case of uncaught errors.
- dvt 1y agoMaybe contrarian, but imo the `Result` type, while kind of nice, still suffers from plenty of annoyances, including sometimes not working with the (manpages-approved) `dyn Error`, sometimes having to `into()` weird library errors that don't propagate properly, or worse: `map_err()` them; I mean, at this point, the `anyhow` crate is basically mandatory from an ergonomics standpoint in every Rust project I start. Also, `?` doesn't work in closures, etc. So, while this is an improvement over C++ (and that is not saying much at all), it's still implemented in a pretty clumsy way.
- maplant 1y ago? definitely works in closures, but it often takes a little finagling to get working, like specifying the return type of the closure or setting the return type of a collect to a Result<Vec<_>>
- deleted 1y ago[deleted]
- ninkendo 1y agoMapping a Vec<T> to Result<U, E> and collecting them into a single Result<Vec<U>, E> made me feel like a ninja when I first learned it was supported. I’m a little worried it’s too confusing to read for others, but it works so well. Combined with futures::try_join_all for async closures and you can use it to do a bunch of failable tasks in parallel too, it’s great.
- ackfoobar 1y ago> the `anyhow` crate is basically mandatory from an ergonomics standpoint in every Rust project I start If you use `anyhow`, then all you know is that the function may `Err`, but you do not know how - this is no better than calling a function that may `throw` any kind of `Throwable`. Not saying it's bad, it is just not that much different from the error handling in Kotlin or C#.
- efnx 1y agoYes. I prefer ‘snafu’ but there are a few, and you could always roll your own.
- tubs 1y agoAnd panics?
- epage 1y agoThose are generally used as asserts, not control flow / error handling.
- mdf 1y agoGenerally, I agree the situation with errors is much better in Rust in the ways you describe. But, there are also panics which you can catch_unwind[1], set_hook[2] for, define a #[panic_handler][3] for, etc. [1] https://doc.rust-lang.org/std/panic/fn.catch_unwind.html https://doc.rust-lang.org/std/panic/fn.catch_unwind.html [2] https://doc.rust-lang.org/std/panic/fn.set_hook.html https://doc.rust-lang.org/std/panic/fn.set_hook.html [3] https://doc.rust-lang.org/nomicon/panic-handler.html https://doc.rust-lang.org/nomicon/panic-handler.html
- ekidd 1y agoYeah, in anything but heavily multi-threaded servers, it's usually best to immediately crash on a panic. Panics don't mean "a normal error occurred", they mean, "This program is cursed and our fundamental assumptions are wrong." So it's normal for a unit test harness to catch panics. And you may occasionally catch them and kill an entire client connection, sort of the way Erlang handles major failures. But most programs should just exit immediately.
- jeroenhd 1y agoThe result type does make for some great API design, but SerenityOS shows that this same paradigm also works fine in C++. That includes something similar to the ? operator, though it's closer to a raw function call. SerenityOS is the first functional OS (as in "boots on actual hardware and has a GUI") I've seen that dares question the 1970s int main() using modern C++ constructs instead, and the API is simply a lot better. I can imagine someone writing a better standard library for C++ that works a whole lot like Rust's standard library does. Begone with the archaic integer types, make use of the power your language offers! If we're comparing C++ and Rust, I think the ease of use of enum classes/structs is probably a bigger difference. You can get pretty close, but Rust avoids a lot of boilerplate that makes them quite usable, especially when combined with the match keyword. I think c++, the language, is ready for the modern world. However, c++, the community, seems to be struck at least 20 years in the past.
- jchw 1y agoGoogle has been doing a very similar, but definitely somewhat uglier, thing with StatusOr<...> and Status (as seen in absl and protobuf) for quite some time. A long time ago, there was talk about a similar concept for C++ based on exception objects in a more "standard" way that could feasibly be added to the standard library, the expected<T> class. And... in C++23, std::expected does exist[1], and you don't need to use exception objects or anything awkward like that, it can work with arbitrary error types just like Result. Unfortunately, it's so horrifically late to the party that I'm not sure if C++23 will make it to critical adoption quickly enough for any major C++ library to actually adopt it, unless C++ has another massive resurgence like it did after C++11. That said, if you're writing C++ code and you want a "standard" mechanism like the Result type, it's probably the closest thing there will ever be. [1]: https://en.cppreference.com/w/cpp/utility/expected https://en.cppreference.com/w/cpp/utility/expected
- a_t48 1y agoThere’s a few backports around, not quite the same as having first class support, though.
- zozbot234 1y ago> The one thing that sold me on Rust (going from C++) was that there is a single way errors are propagated: the Result type. No need to bother with exceptions This isn't really true since Rust has panics. It would be nice to have out-of-the-box support for a "no panics" subset of Rust, which would also make it easier to properly support linear (no auto-drop) types.
- alexeldeib 1y agothat's kind of a thing with https://docs.rs/no-panic/latest/no_panic/ https://docs.rs/no-panic/latest/no_panic/ or no std and custom panic handlers. not sure what the latest is in the space, if I recall there are some subtleties
- zozbot234 1y agoThat's a neat hack, but it would be a lot nicer to have explicit support as part of the language.
- nicce 1y agoThe problem is with false positives. Even if you clearly see that some function will never panic (but it uses some feature which may panic), compiler might not always see that. If compiler says that there are no panics, then there are no panics, but is it enough to add as part of the language if you need to mostly avoid using features that might panic?
- kbolino 1y agoThat's going to be difficult because the language itself requires panic support to properly implement indexing, slicing, and integer division. There are checked methods that can be used instead, but to truly eliminate panics, the ordinary operators would have to be banned when used with non-const arguments, and this restriction would have to propagate to all dependencies as well.
- josephg 1y ago
- dabinat 1y agoI wish Option and Result weren’t exclusive. Sometimes a method can return an error, no result or a valid result. Some crates return an error for “no result”, which feels wrong to me. My solution is to wrap Result<Option>, but it still feels clunky. I could of course create my own type for this, but then it won’t work with the ? operator.
- vjerancrnjak 1y agoThis sounds valid. Lookup in a db can be something or nothing or error. Just need a function that allows lifting option to result.
- dicytea 1y ago> I could of course create my own type for this, but then it won’t work with the ? operator. This is what the Try[^1] trait is aiming to solve, but it's not stabilized yet. [^1]: https://rust-lang.github.io/rfcs/3058-try-trait-v2.html https://rust-lang.github.io/rfcs/3058-try-trait-v2.html
- atoav 1y agoI think Result<Option> is the way to go. It describes precisely that: was it Ok? if yes, was there a value? I could imagine situations where an empty return value would constitute an Error, but in 99% of cases returning None would be better. Result<Option> may feel clunky, but if I can give one recommendation when it comes to Rust, is that you should not value your own code-aesthetical feelings too much as it will lead to a lot of pain in many cases — work with the grain of the language not against it even if the result does not satisfy you. In this case I'd highly recommend just using Result<Option> and stop worrying about it. You being able to compose/nest those base types and unwraping or matching them in different sections of your code is a strength not a weakness.
- Arnavion 1y agoResult<Option> is the correct way to represent this, and if you need further convincing, libstd uses it for the same reason: https://doc.rust-lang.org/stable/std/primitive.slice.html?search=-%3E%20Result%3COption%3E https://doc.rust-lang.org/stable/std/primitive.slice.html?se...
- 90s_dev 1y agoI like so much about Rust. But I hear compiling is too slow. Is it a serious problem in practice?
- zozbot234 1y agoPeople who say "Rust compiling is so slow" have never experienced what building large projects was like in the mid-1990s or so. It's totally fine. Besides, there's also https://xkcd.com/303/ https://xkcd.com/303/
- kelnos 1y agoNot really relevant. The benchmark is how other language toolchains perform today, not what they failed to do 30 years ago. I don't think we'd find it acceptable to go back to mid-'90s build times in other languages, so why should we be ok with it with something like Rust?
- creata 1y agoOr maybe they have experienced what it was like and they don't want to go back.
- mynameisash 1y agoIt depends on where you're coming from. For me, Rust has replaced a lot of Python code and a lot of C# code, so yes, the Rust compilation is slow by comparison. However, it really hasn't adversely affected (AFAICT) my/our iteration speed on projects, and there are aspects of Rust that have significantly sped things up (eg, compilation failures help detect bugs before they make it into code that we're testing/running). Is it a serious problem? I'd say 'no', but YMMV.
- Seattle3503 1y agoAbsolutely, the compile times are the biggest drawback IMO. Everywhere I've been that built large systems in Rust eventually ends up spending a good amount of dev time trying to get CI/CD pipeline times to something sane. Besides developer productivity it can be an issue when you need a critical fix to go out quickly and your pipelines take 60+ minutes.
- cmrdporcupine 1y agoabseil's "StatusOr" is roughly like Rust's Result type, and is what is used inside Google's C++ codebases (where exceptions are mostly forbidden) https://github.com/abseil/abseil-cpp/blob/master/absl/status/statusor.h https://github.com/abseil/abseil-cpp/blob/master/absl/status...
- fpoling 1y agoResult type still requires quite a few lines of boilerplate if one needs to add custom data to it. And as a replacement of exceptions with automatic stack trace attachment it is relatively poor. In any case I will take Rust Result over C++ mess at any time especially given that we have two C++, one with exception support and one without making code incompatible between two.
- jandrewrogers 1y agoFWIW, stack traces are part of C++ now and you can construct custom error types that automagically attach them if desired. Result types largely already exist in recent C++ editions if you want them. I use completely custom error handling stacks in C++ and they are quite slick these days, thanks to improvements in the language.
- fpoling 1y agoWhat I really like to see is stack traces annotated with values of selected local values. A few years ago I tried that in a C++ code base where exceptions were disabled using macros and something like error context passed by references. But the result was ugly and I realized that I had zero chances to adopt it. With Rust Result and powerful macros it easier to implement.
- kccqzy 1y agoThe Result type isn't really enough for fun and easy error handling. I usually also need to reach for libraries like anyhow https://docs.rs/anyhow/latest/anyhow/ https://docs.rs/anyhow/latest/anyhow/. Otherwise, you still need to think about the different error types returned by different libraries. Back at Google, it was truly an error handling nirvana because they had StatusOr which makes sure that the error type is just Status, a standardized company-wide type that stills allows significant custom errors that map to standardized error categories.
- 0x1ceb00da 1y agoProper error handling is the biggest problem in a vast majority of programs and rust makes that straightforward by providing a framework that works really well. I hate the `?` shortcut though. It's used horribly in many rust programs that I've seen because the programmers just use it as a half assed replacement for exceptions. Another gripe I have is that most library authors don't document what errors are returned in what situations and you're left making guesses or navigating through the library code to figure this out.
- bena 1y agoOk, I'm at like 0 knowledge on the Rust side, so bear that in mind. Also, to note that I'm genuinely curious about this answer. Why can't I return an integer on error? What's preventing me from writing Rust like C++?
- tczMUFlmoNk 1y agoYou can write a Rust function that returns `i32` where a negative value indicates an error case. Nothing in Rust prevents you from doing that. But Rust does have facilities that may offer a nicer way of solving your underlying problem. For instance, a common example of the "integer on error" pattern in other languages is `array.index_of(element)`, returning a non-negative index if found or a negative value if not found. In Rust, the return type of `Iterator::position` is instead `Option<usize>`. You can't accidentally forget to check whether it's present. You could still write your own `index_of(&self, element: &T) -> isize /* negative if not found */` if that's your preference. https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.position https://doc.rust-lang.org/std/iter/trait.Iterator.html#metho...
- bonzini 1y agoNothing prevents you, you just get uglier code and more possibility of confusion.
- ryandrake 1y agoError handling and propagation is one of those things I found the most irritating and struggled[1] with the most as I learned Rust, and to be honest, I'm still not sure I understand or like Rust's way. Decades of C++ and Python has strongly biased me towards the try/except pattern. 1: https://news.ycombinator.com/item?id=41543183 https://news.ycombinator.com/item?id=41543183
- skrtskrt 1y agothere are answers in the thread you linked that show how easy and clean the error handling can be. it can look just like a more-efficient `except` clauses with all the safety, clarity, and convenience that enums provide. Here's an example: * Implementing an error type with enums: https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main/src/api/k2v/error.rs https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main/... * Which derives from a more general error type with even more helpful enums: https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main/src/api/common/common_error.rs https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main/... * then some straightforward handling of the error: https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main/src/api/common/generic_server.rs#L228-L240 https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main/...
- zaphar 1y agoCounterpoint: Decades of C++/Python/Java/... has strongly biased me against the try/except pattern. It's obviously subjective in many ways. However, what I dislike the most is that try/except hides the error path from me when I'm reading code. Decades of trying to figure out why that stacktrace is happening in production suddenly has given me a strong dislike for that path being hidden from me when I'm writing my code.
- sham1 1y agoThere should be a way to have the function/method document what sort of stuff can go wrong, and what kinds of exceptions you can get out of it. It could be some kind of an exception check thing, where you would either have to make sure that you handle the error locally somehow, or propagate it upwards. Sadly programming is not ready for such ideas yet. --- I jest, but this is exactly what checked exceptions are for. And the irony of stuff like Rust's use of `Result<T, E>` and similarly ML-ey stuff is that in practice they end up with what are essentially just checked exceptions, except with the error type information being somewhere else. Of course, people might argue that checked exceptions suck because they've seen the way Java has handled them, but like... that's Java. And I'm sorry, but Java isn't the definition of how checked exceptions can work. But because of Java having "tainted" the idea, it's not explored any further, because we instead just assume that it's bad by construction and then end up doing the same thing anyway, only slightly different.
- loeg 1y agoI work in a new-ish C++ codebase (mid-2021 origin) that uses a Result-like type everywhere (folly::Expected, but you get std::expected in C++23). We have a C pre-processor macro instead of `?` (yes, it's a little less ergonomic, but it's usable). It makes it relatively nice to work in. That said, I'd prefer to be working in Rust. The C++ code we call into can just raise exceptions anywhere implicitly; there are a hell of a lot of things you can accidentally do wrong without warning; class/method syntax is excessively verbose, etc.
- flohofwoe 1y agoIMHO the ugly thing about Result and Option (and a couple of other Rust features) is that they are stdlib types, basic functionality like this should be language syntax (this is also my main critique of 'modern C++'). And those 'special' stdlib types wouldn't be half as useful without supporting language syntax, so why not go the full way and just implement everything in the language?
- choeger 1y agoUh, nope. Your language needs to be able to define these types. So they belong into the stdlib because they are useful, not because they are special. You might add syntactic sugar on top, but you don't want these kinds of things in your fundamental language definition.
- stodor89 1y agoFailure is not an option, it's a Result<T,E>
- chickenzzzzu 1y agowhy not just read the function you are calling to determine the way it expects you to handle errors? after all, if a library exposes too many functions to you, it isn't a good library. what good is it for me to have a result type if i have to call 27 functions with 27 different result types just to rotate a cube?
- fooker 1y agoOne of the strengths of C++ is the ability to build features like this as a library, and not hardcode it into the language design. Unless you specifically want the ‘?’ operator, you can get pretty close to this with some clever use of templates and operator overloading. If universal function call syntax becomes standardized, this will look even more functional and elegant.
- steveklabnik 1y agoRust also started with it as a library, as try!, before ?. There were reasons why it was worth making syntax, after years of experience with it as a macro.
- scotty79 1y ago> Then you top it on with `?` shortcut I really wish java used `?` as a shorthand to declare and propagate checked exceptions of called function.
- tomp 1y agoDid you ever actually program in Rust? In my experience, a lot of the code is dedicated to "correctly transforming between different Result / Error types". Much more verbose than exceptions, despite most of the time pretending they're just exceptions (i.e. the `?` operator). Why not just implement exceptions instead? (TBH I fully expect this comment to be downvoted, then Rust to implement exceptions in 10 years... Something similar happened when I suggested generics in Go.)
- nomel 1y agoI've only worked in exceptions, so I can't really comprehend the book-keeping required without them. To me it's a separation of concerns: the happy path only involves happy code. The "side channel" for the unhappy path is an exception, with an exception handler at a layer of the abstraction where it's meaningful, yet happy, code. By "happy" I mean code that's simply the direct practical work that's trying to accomplished something, so doesn't need to worry about when things go terribly wrong. Being blind to the alternative, and mostly authoring lower level libraries, what's the benefit of not having exceptions? I understand how they're completely inappropriate for an OS, a realtime system, etc, but what about the rest? Or is that the problem: once you have the concept, you've polluted everything?
- alpaca128 1y agoIn places where you only want to pass an error to the caller, Rust lets you just add "?" to the call that returns a Result/Option. And the Rust compiler will check that every potential error is handled. I wouldn't say that it's the tedious part of the language.
- tomp 1y agoThere's mostly drawback of having exceptions, I'm not really aware of any benefits of not having them. People often complain about: - performance impact, either speed (most languages) or binary size (C++); this, however, is mostly an implementation concern, and doesn't impact Rust at all, as exceptions can simply be syntax-level compiler sugar, and can be compiled exactly the same as Result type is currently (having said that, the optional stack trace is another potential issue, which would have a performance impact even in Rust) - checked exceptions - this is particularly a concern in Java, which has a fairly poor type system (closed subtyping only) and no type inference, so declaring all unhandled exceptions is tedious - non-checked exceptions - in this case, "every exception can happen anywhere" so it's ostensibly unsafe (well it's just how life is, and even Java has special unchecked exceptions such as OutOfMemory that can happen anywhere) - some people claim that "exceptions can't just randomly jump out of code" is a benefit of not having exceptions but usually those people sweep OutOfMemory and DivisionByZero under the rug (e.g. Rust, where they just "crash" the program) Rust would obviously fit the checked exceptions path, as the Result implementatoin basically is "poor-man's checked exceptions". It only needs to flip the syntax sugar - propagate by default (and implicitly), "catch" and materialize using `?` - as well as making Error an open enum (such that you can add cases implicitly - see e.g. OCaml's exception type `exn` [1]) - and that's basically it! [1] https://ocaml.org/manual/5.3/extensiblevariants.html https://ocaml.org/manual/5.3/extensiblevariants.html
- hoppp 1y agoIts true but using unwrap is a bit boring , I mean...boring is good but its also boring.
- craftkiller 1y agoYou shouldn't be using unwrap.
- throw10920 1y agoThe result type is obviously insufficient for writing nontrivial programs, because nontrivial programs fail in nontrivial ways that need exceptional control flow. The result type does not work because you have to choose between immediate callers handling failures (they don't always have the context to do so because they're not aware of the context of callers higher up on the call stack) or between propagating all of your error values all the way up the stack to the error handling point and making your program fantastically brittle and insanely hard to refactor.
- ttfkam 1y agoThe Result type works for an awful lot of people. Be careful with absolute statements like "does not work." When it works for many others, they might just assume it's a skill issue.
- throw10920 1y agoWhen I say "it doesn't work" I mean that it doesn't allow you to write good code, not "doesn't work" as in the sense that people don't like it. That latter one doesn't make any sense, as languages like PHP "work" for many tens (hundreds?) of thousands of people. I'm well aware of the tendency of Rust programmers to write bad code, constrained by the language, and then be deluded into thinking that that's good code.
- ttfkam 1y agoLord, grant me the confidence of this man who claims objective understanding of what does and does not constitute "good code".
- throw10920 1y agoI note that you did nothing to refute my point about why error-handling-via-return-values is insufficient and instead resort to emotional manipulation and logical fallacies. This seems to happen a lot in the Rust community when people point out flaws in the language.
- divan 1y agoConvention-wise Go is even better. On the one hand, there is zero magic in error handling ("Errors are values" and interface type 'error' is nothing special), on the other hand it's kind of a convention (slightly enforced by linters) that functions that return errors use this type and it's the last return parameter. Nothing prevents people from doing their own way (error int codes, bool handling, Result types, etc, panic), but it's just an easiest way that handles well 99% of the error handling cases, so it sticks and gives a nice feeling of predictability of error handling patterns in Go codebases.
- ttfkam 1y agoIt's also highly dependent upon the team's skill and diligence. You can easily ignore errors and skip error handling in Go with predictably hilarious results. In Rust, you can't just skip error handling. You have to proactively do something generally unwise (and highly visible!) like call .unwrap() or you have to actually handle the error condition. Go still relies on goodwill and a good night's sleep. The Rust compiler will guard against laziness and sleep deprivation, because ultimately programming languages are about people, not the computers.