Macros like try! might take up less space, but is that really a bottleneck for your work?
Macros like try! might take up less space, but is that really a bottleneck for your work?
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.
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.
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.
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.
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
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 ) 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).
So, you call them ignorant (incredibly ignorant, at that) because they're not using a language with exceptions or pattern matching, and they're wasting they're time on "C-style" error handling and... what? They're ignorant until they use something else?
You're also drawing a weak conclusion here: if you use Go idomatically, which requires more lines on your screen for error handling, and a majority of programming errors are a result of the programmer not being able to keep a sufficient amount of program state in their head because of code not fitting neatly on the screen, then using Go leads to more bugs.
Do you have anything that substantiates that?
Honestly, yes, though I would try to phrase it more constructively. If you've only seen C-style languages, it's easy to be ignorant of the world out there; it's easy to try two or three languages and think you know what's going on, when in fact the languages you know all come from a very similar lineage. Like learning English, German and Dutch, and then assuming you know the general principles of human grammar.
If you've tried a language pattern matching and higher kinded types, and found them to be not useful, then fair enough. But you owe it to yourself to try, and even if you end up not using the feature, understanding it will make you a better programmer.
> Do you have anything that substantiates that?
Over many years of research into what causes bugs, there's only one thing we've ever been able to correlate reliably with number of bugs: number of lines of code. Even across different languages, the correlation holds: http://programmers.stackexchange.com/questions/185660/is-the... . So the only evidence-based way to reduce software errors is to reduce lines of code - something you can do by switching to a more expressive language.
I also think you're correct about everything you said, especially about the inconsistent treatment of errors in Golang's stdlib, and the difficulty of differentiating between them. I've noticed the same and it has annoyed me. When writing my own application code I do maintain a consistent pattern for returning my own errors, but I have been frustrated when consuming the stdlib for this very reason.
Maybe as my code base grows I'll come to agree with you, but for now at least it seems to be at a nice middle ground where the error handling isn't overwhelming and it isn't swept under the rug. And to my knowledge, none of the bugs I've created when writing Go code have come from my understanding of the program flow being interrupted by error handling code. And I've probably avoided many bugs because of this pessimistic approach I take when coding, as jalfresi put it in his comment[1].