Things I Enjoy in Rust: Error Handling
blog.jonstodle.com
blog.jonstodle.com
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.
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.
Some of these tensions seem unresolvable though. At some level if I want to toss a Map around everywhere and be able to transparently back it with a database, I'm going to have to accept that operations might fail. I can't have it both ways. In the end, the abstraction is "leaky". I don't really see how Rust or anything else can make that go away.
I feel like the closest I can get to have my cake and eat it too is by having the compiler transparently infer all the exceptions / errors that can be thrown by the calls I am making (like type inference for variables), but leave them out of the type signatures. Then I can opt in to enforcing the checking of them when I want to, so something like
check {
doSomething()
}
catch(...) { // every error now has to be handled
....
}
I guess this is something like what Rust is achieving through the ? operator.Alternatively, you can just say that map is something that never throws, by definition. But, of course, that reduces the utility of the interface, by making it less generic than it could be. In some cases - especially with HOFs - the reduction in utility can be so substantial that it negates any gains from code reuse.
https://doc.rust-lang.org/book/ch09-02-recoverable-errors-wi...
Rust doesn't have higher kinded types, every closure has a unique type that implements up to three different traits, and it has imperative control flow structures that don't exist in Haskell, et al.
By the time you've solved all these problems, you've moved away from monads to full-blown delimited continuations, one small step away from an effect system. And nobody really knows how to make that work while still providing Rust/C++-like "zero cost abstraction."
It might be nice, but it's not at all clear that it would even be "just one interface" anyway.
This wasn't directed at Rust in particular; many languages have these kinds of operators and even more are trying to propose them and I wonder if people just don't see the big picture and/or don't care. You can have a good and productive language without seeing the big picture or maybe realizing too late, but it's not a paragon of good design. It's definitely a design smell to introduce special-cased operators for these things.
With that said, I really like Rust and I think it's an excellent language. This particular thing irks me because a lot of the people who'd exalt this kind of feature usually simultaneously argue that monads are somehow not relevant, while not realizing it's contradictory.
The people who made these choices, and the people who "exalt" them, thus cannot fairly be described as "just don't see the big picture," "don't care," or "not realizing it's contradictory." And until someone has figured out a way to combine a general monadic interface (or more likely, some other method for orchestrating continuations) with Rust-like zero-cost abstractions, these features continue to be "cool" "in their own right." :)
From the standpoint of "How do we handle things that behave like these things (monads) in a general way?" it's certainly not a good one. It's not that I'm saying that Rust is badly designed overall, but like other languages with these operators it exhibits this particular design smell. It's not really debatable that this kind of special-casing is bad design and lack of foresight or a general lack of caring about this thing in particular.
It's also fine not to care about it, just as it's fine to not care about generics if one so chooses.
> The people who made these choices, and the people who "exalt" them, thus cannot fairly be described as "just don't see the big picture," "don't care," or "not realizing it's contradictory." And until someone has figured out a way to combine a general monadic interface (or more likely, some other method for orchestrating continuations) with Rust-like zero-cost abstractions, these features continue to be "cool" "in their own right." :)
I think you're reading a bit more into this than I intended and I guess that's fair. I don't think it's that big a deal that Rust lacks a way to generalize over this concept, but let's not pretend it's a good thing. At which nth special operator for some variant of this behavior would you consider this to be generally not a great design choice? It's either some N or where you intentionally limit this simply because making a bunch of operators is a silly proposition. Either of those cases mean a failure in design. Is it a major one that matters to everyone? No, but it's a shortcoming.
It's fine for languages to make trade-offs and not be perfect in every way, I don't think it's terribly productive to not see obvious shortcomings just because we like something.
That's what I'm reacting to, mostly. But also to the idea that it's some kind of universal, uncontroversial positive for a language to abstract over an unbounded number of implementations of monads.
In a tautological sense, of course, if a language doesn't support feature X that's a problem for people who want to use feature X. But we don't write programs to use language features- we write programs to solve problems.
That is, one could argue that feature X has too much complexity to be worth having in a language, and that solving the same problems another way (or not at all) is better. This is different from the point I made up-thread, and I'm still not trying to make it about monads. But it does seem to me that classifying every tradeoff as a shortcoming presupposes that if a language could support every single possible feature it would be perfect, which is also a silly proposition.
But it's pretty unreasonable to call it _bad design_ in a vacuum, as if there are no obstacles to a more generic design. The reason something like do-notation wasn't chosen is that nobody figured out a way to implement it without major trade-offs. So the alternative you're championing here either isn't possible within the constraints of the language, or the solution hasn't been discovered yet. The `?` operator was a pragmatic compromise.
I'm with you 100% that it is a shortcoming, but you can't really call it bad design given the reality of the situation. Compromises aren't bad design.
Not a fan of Result, either - it ties together the error type and the type of the desired item - these should be separate concerns.
How so? 'Result' is just a generic variant record with two variant cases, for the "success" and "error" condition. It's the exact same as the `value, err` pattern in Go, except that it additionally enforces proper semantics and handling of the outcome.
To be clear, that isn't the fault of `?` but that Rust "requires" you to specify your error type. You could just specify `Box<Error>` and go about your day without boiler plate.
What you mention and my workaround are two extremes of the solution space. There are Rust crates inbetween that help people handle errors the way they need with minimal boiler plate.
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.
impl From<QuestionError> for ReturnError {
fn from(error: QuestionError) -> ReturnError {
// TODO
unimplemented!()
}
}It it safe to just put a noop in there? What's From-ness? Is QuestionError defined in std? Where do I read more?
EDIT: oh I see, I didn't read carefully. I just need to provide a conversion. Your example is very clear.
Someone elsewhere in this thread linked to an example of `impl From<io::Error>` and IMO that's a clear, explicit example of how to do it.
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.
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)?
Of course, that's just one anecdotal example, but I'd encourage you, dep_b, to look for openings. I'd guess that only very few companies use Rust exclusively right now, because of its relatively young age and because it frankly doesn't make sense everywhere, but many use another main language plus Rust.
One had to make an active effort to pick Python to be a tool for the job.
I remember being in one of big government project pitch session and the project manager asked "What this Python language? Sounds like some toy."
Times will change. However Rust sweet spot is system programming. Even though one could build e.g. websites with it, Rust loses its edge and becomes verbose, a too heavy hammer.
OneSignal (SFBA) and Sentry (SF, Austria) use Rust. I was actually surprised half a year ago when I started looking for companies using Rust that there were many more than I expected.
Rust, on the other hand, has a lot of momentum behind it in the embedded space. Bare metal and embedded RTOS options.
Rust is slowly gaining momentum. Jobs are starting to pop up here and there.
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?
Go's way of providing try!() macro is less magical but almost as useful.[1]
> From/Into error types allow the compiler to wrap types or convert between error types
error is an interface in Go which can be easily cast/checked for the underlying type.
> Go's error handling is pretty primitive
I wouldn't call it primitive, I would call it simple. I like the comparison i read on a blog on HN.
Rust is the 'new' C++ and Go is the new C
Yeah it's done during runtime and the compiler won't be able to help you with it if you fail to do exhaustive type checking. It's a problem anytime you refactor your code. ADTs and pattern matching is pretty much the bare minimum language feature i expect from any statically typed language.
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.
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.
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.
I'm not sure I would call this an advantage. See: Handling of Checked Exceptions in Java.
But that can only happen if the method returns a type of Result, true? Does this have the effect that just about every function ends up having to return Result (so that the ? operator works)?
That's better, not worse, because it means you don't up with unhandled downstream errors bloating the signature just because people didn't want to bother with them, which means people are more likely to do something sensible (which may just be wrapping) with them.
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.
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:
myResult.map(<do-something>).and_then(sq).or_else(<do-something-else>);
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
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.
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`.
Nope. I think unwrap shouldn't be used without understanding the error handling capabilities of Rust. It also shouldn't be used for deployed software. The problem is that most people introduced to Rust would like to see something working now and Rust has higher barrier to entry than most mainstream languages. They'll likely keep the habit.
> From the function's perspective, it errored, but from your perspective, it failed and you want to fallback to a default value.
That's not my point. My point is that a failed function should not return a "default value". A default value is not a failure but rather a design decision. Should the function fail, it returns an error.
This is equivalent to saying that `&vector[i]` shouldn't be used in deployed software, since it's just syntactic sugar for `vector.get(i).unwrap()`. It's also equivalent to saying that `assert!(...)` should never be used in deployed software. Or `unreachable!()`. Or any number of other things.
unwrap is perfectly fine for use in production. Stating a requirement in terms of unwrap doesn't make sense outside of niche scenarios. It's too specific and not really actionable in a lot of cases. A better way to put it is that programs that surface a panic to end users of the application have a bug. If the panic is from inside of a library and not the result of documented preconditions on a function, then the library has a bug. Otherwise, the bug is in the application.
>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).
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.[0] https://doc.rust-lang.org/stable/book/ch09-02-recoverable-er...
What I would really like to see in C#:
public int DoThing(int y)
{
//logic
}
catch (SomeException e)
{
//handle things
return 0;
}
This would remove a lot of useless syntactic garbage without the need for any radical changes in the language. (It would translate to try-catch that wraps the entire body of the method.)But so far I gave up on syntax-level error handling of any complexity. Generally, I just write multiple "agents" and all the error handling, retries and recovery is simply done by separate agent(s). The agent that tries to perform the initial actions simply records that it failed in some persistent storage and terminates or goes on to do something else.
It would not not be the same unless you change how scoping works in these instances, and that can introduce other complexities, e.g. how to limit scope so that some things are inaccessible to the catch block.
public void DoThing(){
//code
} catch (Exception x){
} finally {
}
would simply be syntactic sugar for this: public void DoThing(){
try {
//code
} catch (Exception x){
} finally {
}
}
It's consistent.i.e.
public void DoThing()
{
var someVar = SomeStaticClass.FooFunc();
someVar.DoSomething();
}
catch (Exception x)
{
// Scoping rules will need to change
// to put someVar in scope:
_logger.Log($"{someVar.GetId()} is invalid");
}
finally
{
someVar.Cleanup();
}
And this also implies that there's no way to make a variable declared inside the method inaccessible to the catch block without additional changes to the language.In majority of cases I really don't care about the variables created inside. If I can't handle an error just by looking at the exception and method arguments I usually refactor. In my view, error handling should not be intertwined with the "happy path". It makes reasoning about non-trivial code much harder.
For other cases the old syntax will still be available.
And yes, the value of "finally" in such a shorthand is dubious. I just added it to make it clear how the rewrite rule would work.
https://github.com/tensorflow/tensorflow/blob/38c762add3559b...
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.
You generally have to treat `E` as an error because of the semantics of `.map()` in every language, though, because you have to essentially ignore one of the sides. `map<Result<T, E>>` is untypable for any practical scenario where both sides matter equally.
Also, just to clarify how you can have essentially the same thing you would in f.e. Haskell but in TypeScript:
```
export type Result<T, E> = Ok<T, E> | Err<T, E>;
interface Ok<T, E> {
type: "Ok";
unwrap(): T;
map<B>(f: (x: T) => B): Result<B, E>;
toMaybe(): Just<T>;
or(errorValue: T): T;
apply<U>(f: Result<(x: T) => U, E>): Result<U, E>;
bind<U>(f: (x: T) => Result<U, E>): Result<U, E>;
}
interface Err<T, E> {
type: "Err";
unwrap(): E;
map<B>(f: (x: T) => B): Result<B, E>;
toMaybe(): Nothing<T>;
or(errorValue: T): T;
apply<U>(f: Result<(x: T) => U, E>): Result<U, E>;
bind<U>(f: (x: T) => Result<U, E>): Result<U, E>;
}
```For development purposes this is all essentially the same as proper sum types, _with worse ergonomics_.
My point was mostly that practically speaking you can have a full featured `Result<T, E>` type that is certainly good enough. What TypeScript lacks is what most halfway languages lack; higher-kinded types, etc., and good ergonomics that follow, such as type classes. These are also not a given in more esteemed type system circles: OCaml doesn't have them either (and most features that are "coming soon" in OCaml are bordering on vaporware).
Most nice type system features you can get by way of combinations of 2-3 distinct type system features in TypeScript. In terms of end results the only things that differentiate TypeScript from languages with more traditional type systems is that you have to use `TSLint` to absolutely eradicate `any` from usage completely (wheras you don't have it at all in a better type system) and the ergonomics for functional programming just aren't that great in JS.
This isn't the TS part of the syntax that's the issue, it's just that anyone who's used a ML descendant in the last 40+ years will have noticed that (automatic) currying is a massive boon to functional programming (and on top of that C-syntax is just about the worst for FP with all the noise that comes with just calling functions). Even something small as not being able to define operators is pretty disruptive to nice code. People like to rag on languages that let you do this, but there are many patterns in FP that absolutely are best encoded via well known and generally accepted operators.
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")
if(TrySomeOperationThatMightFail(out var value))
{
}
But even with that I often wish it was more like the Rust example1) 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.