> But, since good programs don’t panic, and neither do good programmers, it’s very rare that using unwrap or expect is actually the right thing to do.
I respectfully disagree.
The assert!() macro is a way to document invariants. It's a bug if a variant is violated. It shouldn't have happened, but if it happens, then there's nothing the user can do except reporting the crash.
The unwrap() and expect() method document the invariant that the None and Err() variants shouldn't occur.
It's fail fast.
You should use error handling only if users would be able to handle the errors.
Tell the end user that the file they wanted to open is not readable, for example. Tell the users of your library that an error happened, like a parse error of a config file. And so on. Tell the users what they can fix themselves.
A bug in your library or program, that's something different. Fail fast! And Rust panics are perfect for that.
The easiest counterpoint to this is to think about a HTTP request. If your library is called by one route, and is known to fail in certain circumstances, this failure should not bring down the entire system. A well designed library should generally not make the choice to crash a system - that's the caller's decision. Yes there are exceptions to this, which is why TFA stated this as "rare" not "never".
Using the assert macro in your code is (in my experience) generally bad. If your code is written well, you can never test that code path. Document invariants with tests instead, or better yet with infallible code.
I think you’re misunderstanding the “fail fast!” advice. You want to fail fast in development not in production. In production you want maximum robustness. Users would rather see an error saying “whoopsie, maybe try that again later” than for the program to exit. That’s part of the reason why the functional error handling patterns are becoming so popular these days. They force you to handle errors and give you type-level info about how the program can fail.
I want my programs to fail fast in production too, because it makes it less likely that even bigger problems will arise. There are many problems that are much worse than a program crashing.
Maybe not perfect, but it seems to work out better than exceptions. Exceptions are a good idea which turned out to be too complicated.
A language has to use destructors to clean up for almost everything for this to work. "?" has no "catch" clause within the function. So if an object has an invariant, and that invariant must be restored on error, the destructors must restore the invariant. If that just means unlocking locks, closing files, or deleting data structures, that works. If some more complex invariant needs to be restored, "?" isn't enough. The calling function has to clean things up. This usually means encapsulating the function that does the work in a function that handles errors and cleanup. Basically, a try block.
There are certain obvious (and some less obvious) benefits to both exceptions and results, but I get the impression a lot of programmers have overreacted against exceptions.
Exceptions "just work" the same in every codebase and require little boilerplate in most languages. I think results really shine for internal business logic where errors are more "invalid" than "exceptional."
> Spend any amount of time in programming circles, and just as the sun rises and falls, you are certain to hear someone complain about error handling in Go. These complaints are, anecdotally, rarely well thought out suggestions on what error handling could or should be like in a language like Go, but often merely boil down to “I don’t like having to look at it”.
I read it. The first paragraph dismisses preferences in a matter that boils down to preference.
> Note that for any sufficiently complex program that invokes many dependencies, this stack trace will be so far down the chain that you may not even see where you’re making the call that causes it.
I don't understand this part. Why would my code not appear in the stack trace? Did this author know how to read stack traces?
Personally I find it much faster to pinpoint errors in the Go style.
With a stack trace, I have to cross-reference with code (ensuring versions match) and filter out a bunch of irrelevant calls in the stack. It’s not uncommon for the stack trace to end deep in library code with the root cause being many calls removed, making me check through a bunch of call sites to figure out what happened.
In Go if good context is added to errors, an error log is generally enough on its own to make it obvious exactly what went wrong.
This is a very long post to say "Go's Error Handling would be better if it was like Rust's, but the language designers made a mistake early-on and now we don't want to fix it"
I understand that writer's position that they can't go back and fix it now, but in my mind "We can't very well fix it now" is quite different from perfect. I think such a rationale justifies every choice as equally "perfect" and is thus useless.
Except for anything more production quality, one needs to lean on third party crates to compose errors without explicitly write tons of boilerplate composing result types.
We're on the like, third (fourth?) generation of error handling crates, and while there's some degree of consensus happening, I'm not sure that it's time for it yet.
If Rust had adopted error_chain into the stdlib, that would have been a huge mistake.
Rust's error handling isn't at all like using a monad. The entire point of being able to express the monad for something like the behavior you expect from error handling is that you write code which automatically propagates the error and in the same breadth prevents you from ever being able to see the error. The result is essentially exactly exceptions: you program the happy path and it entirely hides errors from you, as that's the point of the monad, which for this purpose you can pretty much conceptualize as programmatic flow control (or, even more crudely, an overloaded semicolon / statement separator operator).
Rust is using the data structure, but doesn't have a way to express or use the monad, so you have to deal with and manually propagate the error. But like, if you do not have the monad, the data structure is just awkward... the only reason the data structure for this monad even exists at all is to support the monad, and the reason for the monad is to get a syntax similar to exceptions! In a language with a ton of hard-coded syntax for all of these things you'd use a monad for in Haskell--whether it's error handling, asynchronous execution, scope allocation... whatever floats your boat--you should just use exceptions.
You can choose to work with Rust's Result in a monadic way, that's what methods like Result::and_then and Result::or_else and so on are for.
Because it's just another type you could also do whatever else you like, unlike with the Exceptions in typical languages which have them where too bad, we bolted the information to the control flow so now we're going on a journey.
If you want to bolt control flow to some information in Rust that's fine, feel free to define a function which returns ControlFlow::Break for success if that suits you, the try operator understands what you meant, early success is fine. Actually you can see this reflected in the larger language because break 'label value; exists in Rust unlike for example C++.
I respectfully disagree.
The assert!() macro is a way to document invariants. It's a bug if a variant is violated. It shouldn't have happened, but if it happens, then there's nothing the user can do except reporting the crash.
The unwrap() and expect() method document the invariant that the None and Err() variants shouldn't occur.
It's fail fast.
You should use error handling only if users would be able to handle the errors.
Tell the end user that the file they wanted to open is not readable, for example. Tell the users of your library that an error happened, like a parse error of a config file. And so on. Tell the users what they can fix themselves.
A bug in your library or program, that's something different. Fail fast! And Rust panics are perfect for that.
Using the assert macro in your code is (in my experience) generally bad. If your code is written well, you can never test that code path. Document invariants with tests instead, or better yet with infallible code.
Deleted Comment
A language has to use destructors to clean up for almost everything for this to work. "?" has no "catch" clause within the function. So if an object has an invariant, and that invariant must be restored on error, the destructors must restore the invariant. If that just means unlocking locks, closing files, or deleting data structures, that works. If some more complex invariant needs to be restored, "?" isn't enough. The calling function has to clean things up. This usually means encapsulating the function that does the work in a function that handles errors and cleanup. Basically, a try block.
There are certain obvious (and some less obvious) benefits to both exceptions and results, but I get the impression a lot of programmers have overreacted against exceptions.
Exceptions "just work" the same in every codebase and require little boilerplate in most languages. I think results really shine for internal business logic where errors are more "invalid" than "exceptional."
https://blog.verygoodsoftwarenotvirus.ru/posts/errors-in-go/
I read it. The first paragraph dismisses preferences in a matter that boils down to preference.
> Note that for any sufficiently complex program that invokes many dependencies, this stack trace will be so far down the chain that you may not even see where you’re making the call that causes it.
I don't understand this part. Why would my code not appear in the stack trace? Did this author know how to read stack traces?
With a stack trace, I have to cross-reference with code (ensuring versions match) and filter out a bunch of irrelevant calls in the stack. It’s not uncommon for the stack trace to end deep in library code with the root cause being many calls removed, making me check through a bunch of call sites to figure out what happened.
In Go if good context is added to errors, an error log is generally enough on its own to make it obvious exactly what went wrong.
Something that should be supported directly.
> Something that should be supported directly.
Rust has "adapted" some crates into stdlib in the past. Are there any efforts to that for error handling?
If Rust had adopted error_chain into the stdlib, that would have been a huge mistake.
That is the kind of excuse we give in C and C++ land, using static analysis isn't that bad.
Is Rust doing something different?
Rust is using the data structure, but doesn't have a way to express or use the monad, so you have to deal with and manually propagate the error. But like, if you do not have the monad, the data structure is just awkward... the only reason the data structure for this monad even exists at all is to support the monad, and the reason for the monad is to get a syntax similar to exceptions! In a language with a ton of hard-coded syntax for all of these things you'd use a monad for in Haskell--whether it's error handling, asynchronous execution, scope allocation... whatever floats your boat--you should just use exceptions.
Because it's just another type you could also do whatever else you like, unlike with the Exceptions in typical languages which have them where too bad, we bolted the information to the control flow so now we're going on a journey.
If you want to bolt control flow to some information in Rust that's fine, feel free to define a function which returns ControlFlow::Break for success if that suits you, the try operator understands what you meant, early success is fine. Actually you can see this reflected in the larger language because break 'label value; exists in Rust unlike for example C++.