It's interesting to see modern languages tackling checked error handling. Particularly in the light that Java's checked exceptions are widely considered to have been a failure. Yet I wonder if Java had better support for things like union types etc. if it would have turned out differently. After all, it did offer at least potential to achieve what everyone is after: you can write a piece of code and know for sure that you handled every possible type of error.
The most painful aspect of Java's (original) exceptions was how the only mechanism to handle multiple errors as a single case was using inheritance in the exception hierarchy. The result was that they propagated through interfaces and broke downstream code and it became pretty much impossible to design and maintain a stable API without wrapping exceptions excessively which in turn destroys the utility of the exception hierarchy, making the whole exercise futile.
The article seems a little superficial though. Immediately exiting with 'panic' does not seem like a real error handling approach for any realistic application. It doesn't show how you could handle different types of errors (eg: file not found vs I/O error reading file) with different cases. The case it does present is easily handled elegantly by nearly any error handling design.
I think the biggest problem with Java exception specs is that they're not composable. In other words, I can't define a method X that calls another method Y - potentially via dynamic dispatch - and has an exception specification of "whatever Y throws, plus E".
This manifests almost immediately. For example, suppose you wanted to implement a key/value database with disk storage. The logical core interface for it would be java.util.Map, except for the fact that its methods are declared as non-throwing; thus, you cannot propagate IOException from the underlying storage layer, unless you wrap it into unchecked RuntimeException.
Conversely, java.lang.Appendable is a very generic interface for appending characters or sequences of characters to something. But because its designers knew that some of its implementations would be backed by I/O, all mutating methods in that interface are declared as throwing IOException. Thus, any generic code that tries to work with Appendable has to always assue that it can throw, and has to declare itself as throwing the same exception to propagate. And now the user of that generic code passes a no-throw implementation of Appendable to it, but still has to "handle" IOException that they know will never actually happen.
Or come up with some convoluted scheme, like java.util.Formatter does. This one wraps Appendable, but swallows all I/O exceptions to avoid having to propagate them in the most common case, where the wrapped Appendable is a StringBuilder. But since there are other possibilities, it still has to expose some API to examine such exceptions, which it does by providing a method on Formatter that returns the last swallowed exception that was thrown by Appendable. Of course, now you have to remember to actually check it, and you will silently get an incorrect result if you do not, which is worse than unchecked exceptions in the same scenario.
This all gets especially bad one you start working with HOFs, because their contracts are largely defined in terms of the behavior of functions that are passed to them. The old Java lambda proposal by Neal Gafter noted this, and proposed extending the syntax for generics to cover exception specifications (via union typing, indeed). In the end, they added UncheckedIOException to the standard library, specifically so that stuff like Stream.map() can be implemented on top of files, and propagate exceptions, without declaring them.
I've shown Rust's Result type to folks on occasion because I like it so much. The other aspect I like is the `?` operator, which allows for easier error propagation.
I think propagating null operators, `?`, etc., all become a bit less interesting when you consider that mostly they're just specific implementations of a monadic interface and they could all just be represented like that instead. It doesn't seem sensible to me to try to "fake" monads everywhere and add different operators for different ones when we could just have one interface that describes short-circuiting based on the data type.
`?` isn't really that great - for all but toy cases you have to jump through hoops - especially if you are trying to handle different error types from separate in a unified way.
Not a fan of Result, either - it ties together the error type and the type of the desired item - these should be separate concerns.
One of the more interesting semi-recent additions to stable rust is the question mark operator. It's not covered in this Rust Error Handling article, but it's got some of the early-exit appeal of exceptions without too much unwind magic.
Unfortunately I haven't been able to get the question mark operator to work yet with my code, but it looks appealing. It's not 100% obvious to me what qualities my Result type needs in order to be able to make it work. Or whether it's bound in practice to the Result type(s) defined in std. I'm sure those details are all in the docs somewhere but I spent the time to look yet.
One thing you may be missing is that the `Result::Err` in your return type must implement `From` for the `Result::Err` type where you're using the `?` operator. That's already covered if those `Err` types are identical (`From<T> for T`), otherwise you need to implement the conversion.
impl From<QuestionError> for ReturnError {
fn from(error: QuestionError) -> ReturnError {
// TODO
unimplemented!()
}
}
Also, if you use Box<Error> or failure[1]'s Error type you can get around implementing most of the conversion Traits, but you lose the ability to tell which errors can happen by just looking at the type signature and doing exhaustive matching on them.
If you can post your code, we can help you out. One requirement, is that your function returns a Result<Ok, Err>. The Err needs to implement the same type for all the type returned.
This means you can use the "?" operator for functions that you did not create (libraries) but you'll also have to convert their Error type to your Error type.
I'm kind of contemplating a career switch since in mobile JavaScript cross-platform seems to become pretty big and I just don't feel very warmly about becoming a JavaScript specialist.
What are the possibilities for somebody that's pretty heavy iOS senior to transfer to a role building applications or services in Rust (or Elixir, for that matter)?
We (as an organization) are a couple years away from doing Rust for embedded work, but I'll be pushing hard in that direction after I'm more comfortable with it (I'm the tech lead in the organization), in part because I dislike C++ so, so much.
I am a full time Go developer learning rust on the side for more low level things, and one of my side projects is writing an OS.
Rust's error handling really feels the exact same way to Go's in my opinion, the only difference is, so far in my admittedly newcomer viewpoint, using unwrap to get to the error value, or returning an error value and doing an if err != nil check.
This isn't to say anything other than agreement that I indeed like this style. I like to handle my errors as values and program around them.
The thing I don't get is why all the bikeshedding and hate around "if err != nil" spam I seem to have seen here often and on other blogs. Go seems to get this "criticism" (I don't really believe it is a valid critique, for what it is worth) heaped onto it plentifully, while Rust seems generally immune to it, otherwise praised for the approach -- but it is the same approach! In fact you even have a builtin panic func in go!
A lot of the Rust developers I've spoken with don't really consider using unwrap everywhere the way that Go programmers use "if err != nil" to be proper error handling. Most of them consider it an ugly hack to be used when playing around or testing something.
The value that most people see in Rust's error types is in error transformation and propagation, which let you do things like correctly handling edge cases when using closures without cluttering up the logic of the success case.
If you are going to compare unwrap/expect to "if err != nil" -- Rust's advantage is that it forces you to at least pretend to handle the error, while Go will happily keep chugging along even if you never inspect err.
While its true that one can use `unwrap`, pattern matching and other higher order methods available for the Result type are a more idiomatic way to work with these.
For example:
match <something that returns result> {
Ok (v) => <do something with v>,
Err (some error) => <handle error>
}
By using the result type, the compiler ensures that the only way to get a value out of the result type is via pattern matching. So the errors are still values, except there is no need to come up with a default value for the error scenario, and there is no need to come up with an "empty" value.
In isolation it might look verbose, but rust also provides a lot of higher order operations that one can do on result types. Ex:
Something else that I personally like about a result type is that it also serves as documentation by clearly marking operations that can fail, and have a consistent way for the end-user to know which value indicates that the response is a success or an error.
In Go, you don't have to check err != nil to get the underlying value, in Rust you do. You probably aren't missing that exactly, in that you probably knew that difference, but you may be missing its importance. It means that in Rust, it is diminishingly rare to have a value of some type which it is not valid to use, whereas in Go many or most values - any value returned from a function that also returns an error, which is a lot of them - may be invalid (because the error may not have been checked). In mature teams using Go it may be unlikely for any errors to be unchecked, but it is a gun that is always pointed at your foot.
Since Rust supports generics and algebraic data types, you can clearly denote on your return type what kind of errors can be expected and include useful data pertaining to each one, with the compiler making sure everything in its place; as opposed to casting from the error interface in a brittle manner prone to breaking on changes with no help from the compiler.
Since Result is also a functor in practice, you can chain modifications to the happy path without having to check errors when you don't need to and without having to write as much boilerplate.
You can do `.unwrap()` or `.expect(..)` to get your values, and in that respect, it's not that different from working in a language with `null`.
But you can actually almost totally avoid calling `unwrap`/`expect` in Rust code by doing pattern matching.
`
match x {
Some(value) => <DO SOMETHING WITH VALUE>
None => <HANDLE THIS ERROR PATH>,
}`.
If you always do that, then you've got the compiler reminding you to handle the case where it is `null`. That means that you never hit the equivalent of a null exception.
That being said, I still find myself doing some `unwrap` and `expect` (particularly in tests). But relying on matching most of the time as a first line of defense will prevent the equivalent of a null exception in the bulk of your code.
The best parts of rust's error handling are the try!() macro (aka `?`), as well as From/Into error types allow the compiler to wrap types or convert between error types. This eliminates almost all of the go boilerplate `if err != nil` and you only handle the error where you actually need to with pattern matching and destructuring, instead. It is much nicer. Go's error handling is pretty primitive, in my opinion.
It seems like Go's error handling is basically like node's err callback argument, maybe a little cleaner. But it's still not that strict in forcing you to deal with errors. You could just rename err to _ and just drive on.
In Rust, you either have to deal with the error, or explicitly ignore them. And if the error is the type that warrants crashing, that's easy too.
If you'd like to see something that's not "if err != nil", I recommend looking into elixir; there is the option of doing ok/error tuples, and there is a more ergonomic railway programming structure called "with". However, as it is a BEAM language, often the best solution is to "let it fail" as an error handling strategy. Let the process raise and crash, and a supervisor will restart it in a safe state if necessary.
1. The Rust community is biased against the usage of "unwrap". It was supposed to make prototyping easier. It should not have been there and should not be used.
The correct way will be to "evaluate" the function and either return a result or "propagate" the error to some part of your application that will handle "all" the errors. Whether they are your code errors, or errors thrown by other libraries.
2. The code will look even more succinct with the "?" operator:
> let value = some_operation_that_might_fail()?;
3. If the function fail, the program should handle the failure. By design, a failure should not result on setting a "default value". That's not an error but more like an "option".
The correct type the guy should have used is the Option type. Or more like an option inside a result. The option could return a value or not. If not, you set a default value.
> 1. The Rust community is biased against the usage of "unwrap". It was supposed to make prototyping easier. It should not have been there and should not be used.
I'm a little confused on this point you are making. You say the Rust community is biased against `unwrap` which makes it sound like you think that is wrong but then you proceed to say it shouldn't be there, making it sound like you agree.
`unwrap` has legitimate uses in product code as an assertion. Normally I recommend `expect` instead so that the assertion is self-documenting but there are times where the the scope of the assumption you are making is so small, it is obvious and doesn't need a comment, for example if I partition a collection based on the result [0]
> . If the function fail, the program should handle the failure. By design, a failure should not result on setting a "default value". That's not an error but more like an "option".
From the function's perspective, it errored, but from your perspective, it failed and you want to fallback to a default value.
Yes, someone could express this instead as an `Option` or a `Result<Option, _>`
- Sometimes you just don't need that extra boiler plate
- Sometimes you want to provide context for why something failed in case the user considers it an error as well. You can't express that with `Option`.
While I too love Rust's error handling, this explanation of it seems incomplete without discussing the ? operator and its interaction with the From trait. The convenience methods on Result itself are nice of course but they aren't really the main mechanism of error handling in my opinion.
So what do you do when you want centralized error handling, which is the whole point of try-catch idiom?
>he second side effect is that it forces you to think about how to handle possible errors.
Java does this with throws E and (at least in Java) it sucks and I very much prefer C#'s implicit throws. In fact, I don't think it goes far enough. If you have complex error recovery, the language should allow you to completely separate good executions paths from the bad paths. Scope guards are a step in the right direction.
How would Rust code looks like if the error handling logic was something like "try X, if it fails try again, if that fails schedule another attempt via a queue, log a warning; if scheduling fails log a fatal error"?
Sure, handling this stuff with try-catch looks horrible. But I've thought long an hard about how to simplify it and using tricks like .unwrap_or (which you can simulate in C# to some extent) does not cut it, because error handling often requires to operate on method-scoped variables (e.g. for logging).
Depends what exactly you're looking to do. You can propagate errors with the `?` operator. And you can match on the specific error at a higher level.
The docs[0] do give a more complex example of error matching:
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Tried to create file but there was a problem: {:?}", e),
},
other_error => panic!("There was a problem opening the file: {:?}", other_error),
},
};
}
Although they also provide a slightly simpler alternative for this particular case.
TensorFlow has a StatusOr C++ class that allows functions to return a value that can be first checked to be result.ok() (no error), and then the actual value can be unwrapped. It’s Google’s way of not using exceptions.
Designs like Java which have NullPointerException simply ignore null. Modeling T instead of T | null is just too convenient. It's a lazy design.
I'm fine with most of those new ways of error handling, although some feel better and the others feel a little bit awkward. If some concept exists (exception/emptiness/async), model it, handle it. Instead of just ignore it and leak the responsibility to the users.
Not being familiar with Rust, the article was interesting, but I felt like it lacked examples. The Result<T, E> is also available in other languages, even in TypeScript you could have `function x(): number | Error {}`, it is just a consequence of being able to return a union of data types, why is Rust's case special? (apart from being able to handle it nicer with "match")
[+] [-] zmmmmm|7 years ago|reply
The most painful aspect of Java's (original) exceptions was how the only mechanism to handle multiple errors as a single case was using inheritance in the exception hierarchy. The result was that they propagated through interfaces and broke downstream code and it became pretty much impossible to design and maintain a stable API without wrapping exceptions excessively which in turn destroys the utility of the exception hierarchy, making the whole exercise futile.
The article seems a little superficial though. Immediately exiting with 'panic' does not seem like a real error handling approach for any realistic application. It doesn't show how you could handle different types of errors (eg: file not found vs I/O error reading file) with different cases. The case it does present is easily handled elegantly by nearly any error handling design.
[+] [-] int_19h|7 years ago|reply
This manifests almost immediately. For example, suppose you wanted to implement a key/value database with disk storage. The logical core interface for it would be java.util.Map, except for the fact that its methods are declared as non-throwing; thus, you cannot propagate IOException from the underlying storage layer, unless you wrap it into unchecked RuntimeException.
Conversely, java.lang.Appendable is a very generic interface for appending characters or sequences of characters to something. But because its designers knew that some of its implementations would be backed by I/O, all mutating methods in that interface are declared as throwing IOException. Thus, any generic code that tries to work with Appendable has to always assue that it can throw, and has to declare itself as throwing the same exception to propagate. And now the user of that generic code passes a no-throw implementation of Appendable to it, but still has to "handle" IOException that they know will never actually happen.
Or come up with some convoluted scheme, like java.util.Formatter does. This one wraps Appendable, but swallows all I/O exceptions to avoid having to propagate them in the most common case, where the wrapped Appendable is a StringBuilder. But since there are other possibilities, it still has to expose some API to examine such exceptions, which it does by providing a method on Formatter that returns the last swallowed exception that was thrown by Appendable. Of course, now you have to remember to actually check it, and you will silently get an incorrect result if you do not, which is worse than unchecked exceptions in the same scenario.
This all gets especially bad one you start working with HOFs, because their contracts are largely defined in terms of the behavior of functions that are passed to them. The old Java lambda proposal by Neal Gafter noted this, and proposed extending the syntax for generics to cover exception specifications (via union typing, indeed). In the end, they added UncheckedIOException to the standard library, specifically so that stuff like Stream.map() can be implemented on top of files, and propagate exceptions, without declaring them.
[+] [-] uzername|7 years ago|reply
https://doc.rust-lang.org/book/ch09-02-recoverable-errors-wi...
[+] [-] 59nadir|7 years ago|reply
[+] [-] fnord77|7 years ago|reply
Not a fan of Result, either - it ties together the error type and the type of the desired item - these should be separate concerns.
[+] [-] wyldfire|7 years ago|reply
Unfortunately I haven't been able to get the question mark operator to work yet with my code, but it looks appealing. It's not 100% obvious to me what qualities my Result type needs in order to be able to make it work. Or whether it's bound in practice to the Result type(s) defined in std. I'm sure those details are all in the docs somewhere but I spent the time to look yet.
[+] [-] CUViper|7 years ago|reply
[+] [-] rabito|7 years ago|reply
[1]: https://github.com/withoutboats/failure
[+] [-] Falell|7 years ago|reply
[+] [-] csomar|7 years ago|reply
This means you can use the "?" operator for functions that you did not create (libraries) but you'll also have to convert their Error type to your Error type.
[+] [-] ChrisSD|7 years ago|reply
[+] [-] dep_b|7 years ago|reply
What are the possibilities for somebody that's pretty heavy iOS senior to transfer to a role building applications or services in Rust (or Elixir, for that matter)?
[+] [-] rhinoceraptor|7 years ago|reply
[+] [-] ansible|7 years ago|reply
[+] [-] onebot|7 years ago|reply
Rust is slowly gaining momentum. Jobs are starting to pop up here and there.
[+] [-] icxa|7 years ago|reply
Rust's error handling really feels the exact same way to Go's in my opinion, the only difference is, so far in my admittedly newcomer viewpoint, using unwrap to get to the error value, or returning an error value and doing an if err != nil check.
This isn't to say anything other than agreement that I indeed like this style. I like to handle my errors as values and program around them.
The thing I don't get is why all the bikeshedding and hate around "if err != nil" spam I seem to have seen here often and on other blogs. Go seems to get this "criticism" (I don't really believe it is a valid critique, for what it is worth) heaped onto it plentifully, while Rust seems generally immune to it, otherwise praised for the approach -- but it is the same approach! In fact you even have a builtin panic func in go!
Can someone tell me what I may be missing?
[+] [-] ohazi|7 years ago|reply
The value that most people see in Rust's error types is in error transformation and propagation, which let you do things like correctly handling edge cases when using closures without cluttering up the logic of the success case.
If you are going to compare unwrap/expect to "if err != nil" -- Rust's advantage is that it forces you to at least pretend to handle the error, while Go will happily keep chugging along even if you never inspect err.
[+] [-] anuragsoni|7 years ago|reply
For example:
By using the result type, the compiler ensures that the only way to get a value out of the result type is via pattern matching. So the errors are still values, except there is no need to come up with a default value for the error scenario, and there is no need to come up with an "empty" value.In isolation it might look verbose, but rust also provides a lot of higher order operations that one can do on result types. Ex:
Something else that I personally like about a result type is that it also serves as documentation by clearly marking operations that can fail, and have a consistent way for the end-user to know which value indicates that the response is a success or an error.EDIT: Formatting
[+] [-] sanderjd|7 years ago|reply
[+] [-] mixedCase|7 years ago|reply
Since Result is also a functor in practice, you can chain modifications to the happy path without having to check errors when you don't need to and without having to write as much boilerplate.
[+] [-] joshmarlow|7 years ago|reply
But you can actually almost totally avoid calling `unwrap`/`expect` in Rust code by doing pattern matching.
` match x { Some(value) => <DO SOMETHING WITH VALUE> None => <HANDLE THIS ERROR PATH>, }`.
If you always do that, then you've got the compiler reminding you to handle the case where it is `null`. That means that you never hit the equivalent of a null exception.
That being said, I still find myself doing some `unwrap` and `expect` (particularly in tests). But relying on matching most of the time as a first line of defense will prevent the equivalent of a null exception in the bulk of your code.
[+] [-] andrewjf|7 years ago|reply
[+] [-] rhinoceraptor|7 years ago|reply
In Rust, you either have to deal with the error, or explicitly ignore them. And if the error is the type that warrants crashing, that's easy too.
[+] [-] dnautics|7 years ago|reply
[+] [-] csomar|7 years ago|reply
1. The Rust community is biased against the usage of "unwrap". It was supposed to make prototyping easier. It should not have been there and should not be used.
The correct way will be to "evaluate" the function and either return a result or "propagate" the error to some part of your application that will handle "all" the errors. Whether they are your code errors, or errors thrown by other libraries.
2. The code will look even more succinct with the "?" operator:
> let value = some_operation_that_might_fail()?;
3. If the function fail, the program should handle the failure. By design, a failure should not result on setting a "default value". That's not an error but more like an "option".
The correct type the guy should have used is the Option type. Or more like an option inside a result. The option could return a value or not. If not, you set a default value.
[+] [-] epage|7 years ago|reply
I'm a little confused on this point you are making. You say the Rust community is biased against `unwrap` which makes it sound like you think that is wrong but then you proceed to say it shouldn't be there, making it sound like you agree.
`unwrap` has legitimate uses in product code as an assertion. Normally I recommend `expect` instead so that the assertion is self-documenting but there are times where the the scope of the assumption you are making is so small, it is obvious and doesn't need a comment, for example if I partition a collection based on the result [0]
[0]: https://doc.rust-lang.org/stable/rust-by-example/error/iter_...
> . If the function fail, the program should handle the failure. By design, a failure should not result on setting a "default value". That's not an error but more like an "option".
From the function's perspective, it errored, but from your perspective, it failed and you want to fallback to a default value.
Yes, someone could express this instead as an `Option` or a `Result<Option, _>`
- Sometimes you just don't need that extra boiler plate - Sometimes you want to provide context for why something failed in case the user considers it an error as well. You can't express that with `Option`.
[+] [-] bfrydl|7 years ago|reply
[+] [-] gambler|7 years ago|reply
>he second side effect is that it forces you to think about how to handle possible errors.
Java does this with throws E and (at least in Java) it sucks and I very much prefer C#'s implicit throws. In fact, I don't think it goes far enough. If you have complex error recovery, the language should allow you to completely separate good executions paths from the bad paths. Scope guards are a step in the right direction.
How would Rust code looks like if the error handling logic was something like "try X, if it fails try again, if that fails schedule another attempt via a queue, log a warning; if scheduling fails log a fatal error"?
Sure, handling this stuff with try-catch looks horrible. But I've thought long an hard about how to simplify it and using tricks like .unwrap_or (which you can simulate in C# to some extent) does not cut it, because error handling often requires to operate on method-scoped variables (e.g. for logging).
[+] [-] ChrisSD|7 years ago|reply
The docs[0] do give a more complex example of error matching:
Although they also provide a slightly simpler alternative for this particular case.[0] https://doc.rust-lang.org/stable/book/ch09-02-recoverable-er...
[+] [-] BooneJS|7 years ago|reply
https://github.com/tensorflow/tensorflow/blob/38c762add3559b...
[+] [-] namelosw|7 years ago|reply
I'm fine with most of those new ways of error handling, although some feel better and the others feel a little bit awkward. If some concept exists (exception/emptiness/async), model it, handle it. Instead of just ignore it and leak the responsibility to the users.
[+] [-] XCSme|7 years ago|reply
[+] [-] guntars|7 years ago|reply
[+] [-] gamma3|7 years ago|reply
In Scala you return a Validation that is either a value or an error, and can pattern match on it. Also: trySomething().orElse("Default value")
[+] [-] CuriousSkeptic|7 years ago|reply
[+] [-] snypox|7 years ago|reply
[+] [-] CountHackulus|7 years ago|reply
[+] [-] miohtama|7 years ago|reply
1) Is Rust error handling somehow better than in a programming language X?
2) Is it because of Rust or because standard libraries or something else (developer laziness, writing silent exception eating)
3) Why other programming languages choose to do it other ways?
Now the article reads like Rust education, which is good, but other commenters here find it very subjective.