4 ms·
I don't know if I'm just ignorant, but I like that Go forces me to use real estate for dealing with errors. It's the first language I've used where I give as mu
by artursapek 12y ago
I don't know if I'm just ignorant, but I like that Go forces me to use real estate for dealing with errors. It's the first language I've used where I give as much attention and design to the "sad paths" as I do to the "happy path". And I'm very comfortable navigating through text files and folding blocks of code, so it doesn't bother me if handling an error costs me a few lines.
Macros like try! might take up less space, but is that really a bottleneck for your work?
- eropple 12y agoPersonally? Line noise sucks. Constant error checking is vertical line noise, which is even worse; it distracts me from important things like "is my business logic correct" with "did I type that particular error check correctly?". For me, that really sucks. (I get other people like it. That's great. For me, it's junk.) I generally use Scala and just chain through Try[T] monads and address the sad path right at the bottom, right next to the culmination of the happy path. I always know where it is, I can't miss it, I'm not repeating code all over the place and running the risk of screwing up with part of it...to me it's just a smarter way to handle the failure case.
- jalfresi 12y agoPersonally, I don't believe error checking is line noise; it is part of your business logic. Forcing me to acknowledge and deal with error cases requires that I write more robust code, that I acknowledge the failures and errors that can occur within my logic, as they are part and parcel of that logic. If you see only the success path as the "true code" then you are missing the whole raft of other conditions that can occur; its an idealised idea of what the code is. This is why I like Go. Its the first language I've ever used that forces me to think pessimistically about my own code.
- lmm 12y agoScala makes the distinction clear by having the difference between "=" and "<-" (or "←"); the former for something guaranteed to succeed, the latter for a possible error. This is wonderful; it's obvious enough that you see it (and the compiler catches you if you get it wrong), so when an error happens you can immediately see which functions might be causing it and which ones can't be. But you don't have to write a bunch of identical lines; one or two characters are enough for something this common. And you can reuse the same syntax for other error-like contexts, rather than having syntax for this one piece of functionality that's special-cased in your language and no way to handle similar cases (e.g. audit logging).
- eropple 12y agoI don't think you actually read my post before your fingers got going. Monadic comprehension with Try[A] is effectively an Either[A, E], where E is any exception thrown within that context. Mapping over a Try[A] returns a Try[B]; if the first Try failed, it likewise failes, becoming an Either[B, E]. I am thinking about my error case. I am explicitly acknowledging that I am doing so; heck, scalac will fail to compile if you haven't folded across both the failure case and the success case. I'm just not writing the same (stupid, wordy, potentially failure-prone) boilerplate code every time I make a method call into a library.
- jalfresi 12y agoApologies, this is my ignorance re: scala but I have no idea what you just described, although to these ignorant eyes it appears to be a lot more complicated to read and understand than a simple if err != nil check! however, I will go and google some of the concepts you have described to try and understand better your point
- eropple 12y agoIt's not complicated. Your return value can, as in Go, be an error or an object of type T; unlike Go and its stunted type system, this is a generic object and so this must be written once for all types. That return value can be functionally mapped over, just like `map` in Ruby, and will return either the original exception (the mapping function will not be called) or the result of the mapping function. These can be chained indefinitely. At the end you have a Try object of the generic type of the final mapping function, against which you can pattern match for Success or Failure. (Failing to compare against both Success and Failure is a compiler warning. There's a flag, IIRC, to make it an error, but since my IDE is already yelling at me that I screwed up I don't in practice have this problem.) Additionally, at any point in the chain you can call `recover`, which is similar to `map` but for the exceptional case (Try[T] -> Try[T], with Success[T] passed through) if it's a recoverable error. All error handling--as separate from error recovery, which is rarely in practice useful--is always, always, always at the very bottom of the function, right next to the happy path. It literally is impossible to miss, and does not require copy-pasting code after every library function that can fail. There's no reason to check 'err' every time unless your language is just straight-up too dumb to do work for you. Your time is more valuable than the computer's in all but the most extreme of cases; you should act like it.
- TheDong 12y agoThat sounds incredibly ignorant to me. If this is the first language you've used where you give that sort of attention to errors, you must not have used and learned from the terrible mistakes of C, which constantly returns errors which are not required to be handled, just like go. Heck, you must not have learned from bash where every second line is "if [[ $? != 0 ]]" or you just did set -e and fail hard. For me, the problem with Go's errors isn't even the lines they take up, but the fact that they still don't accomplish anything more than C's errors. Since C, we've learned that you can use exceptions to force errors to be handled, or Options + matching to ensure each case is handled or extremely explicitly ignored. We've learned that errors can have contexts like call stacks attached to them. Go ignores all of that and goes back to the C way. It doesn't provide an easy way to chain errors or force them to be handled. It doesn't provide a good idiomatic way to differentiate between the majority of errors from the stdlib because it's discouraged to have multiple error struct types; rather you're encouraged to use errors.New and make an un-differentiatable error type with no good context. It's encouraged to return an err from deep in the stack without including any additional information. In fact, Go's even inconsistent in how it uses errors in the stdlib. For example, if you look at io, you can see the following: http://golang.org/pkg/io/#pkg-variables http://golang.org/pkg/io/#pkg-variables They define their variables as error types which are simply strings. That means you can compare with the constants (e.g. err == io.EOF), but if you add any extra info to that err, such as callstack, you can no longer compare with it. If you look at os, you see "os.IsNotExist(err)" which tells you if an err is of a given type. If you look at net ( http://golang.org/pkg/net/#AddrError http://golang.org/pkg/net/#AddrError ) you can see they implemented their own Error interface implementations which you check by doing `if addrErr, ok := err.(AddrError); ok { // handle error }`.. the fact that the Go authors included three different error patterns in the stdlib already shows that the entire error process in go is poorly thought out. In addition, none of the methods allow you significant flexibility in adding your own info to the error as you pass it up without losing information. If you don't like try! in rust, you're free to write your errors out explicitly, but if try! fits then it's good to use it. The majority of bugs in code come from people being unable to keep a sufficient amount of the program state in their head to track program flow accurately. If half of your program flow is errors, then no wonder. There's an old principal that each function body should fit, in full, on one screen because that ensures it can be read without scrolling and reasoned about fairly easily. Error handling wasting space also wastes thought and makes your code provably harder to reason about (if you don't think that's true, look at the incidence of 'mistake checking error return values in c' vs 'mistake checking error return in haskell' vs 'forgot to either catch or annotate throwable for exception in java'. Yeah).