> doing the whole if err != nil thing every 30-50 lines not ridiculous?
It's more like every 5 lines. I love it as every error is handled correctly and in scope. No error is left behind. Most languages I've used devolve into the GLOBAL MASTER DEFAULT HANDLE ALL ERRORS which is just gross.
In practice, scoping becomes an issue because "err" sneaks in everywhere.
For example, consider:
foo, err := getFoo()
Now your scope has an "err" and you open yourself to logic errors such as:
foo, err := getFoo()
... do stuff ...
if errr := makeBug(); errr != nil {
return err
}
Spot the (heh) error? It's caused by ":=" allowing variables to be reassigned.
You can avoid this dangerous pollution, but it's super ugly:
var foo *Foo
if f, err := getFoo(); err == nil {
foo = f
} else {
return err
}
(You have to have the return in the second branch, otherwise "go vet" will complain. Sigh.)
I have lots of these, and they're not helped by the fact that Go has no type-level mechanism to guard against the misuse of multiple-return values. That is, in the above, either "foo" or "err" are usually set, and if "err" is set, it means "foo" isn't. But the type system doesn't prevent you from accidentally ignoring "err" and using "foo", or vice versus. This gets even worse if the func returns a (non-pointer) struct as its value.
The fact that Go allows shadowing (exacerbated by the convention of using "err" as every single error variable) and promotes the reuse of variables (e.g. the above ":=", or declaring "err" outside a loop) also means it's super easy to assign the wrong thing.
All of the above are my single biggest source of bugs during development. That, and how accidentally using nil/zero values, including the damnable nil map/slice cases.
"go vet" (whose shadowing check is disabled by default (!)) and the various linters only go some ways towards helping here. The errcheck linter (which you can enable through gometalinter) is absolutely essential.
You almost always defined your variables. None of this `foo, err := getFoo()` nonsense.
Most functions return an error so err is already defined:
func doIt(a, b) (result string, err error) { ... }
Then you are free to reuse err as needed.
func doIt(a, b int) (result string, err error) {
var x, y string
x, err = doA(a)
if err != nil {
return
}
y, err = doB(b)
if err != nil {
return
}
result = x + y
return
}
That solves nothing. The point was that you pollute the scope with error variables, leading to dangerous logic errors that can be hard to spot. Your snippet is no better than:
There are real problems, and not real problems. I'd classify this as a not real problem. How hard is it to name errors differently when it would be a real problem? A few keystrokes.. and BOOM! Problem solved.
I wonder what is the correlation between volume of complaining about minutia such as this compared to real life ability and level of accomplishment.
> I love it as every error is handled correctly and in scope.
Not in scope. Because you are forced to deal with the error right here right now, sometimes it's in scope and sometimes, you're simply not able to do anything with the error so you effectively mishandle it.
Exceptions allow you to handle the error where it actually makes the most sense.
There are options between these two extremes. Haskell's Except monad is a great example. Or javascript promises. You can launch a bunch of actions in sequence and provide a single error handler for them. Each step doesn't have to worry about it, but neither is the error handling invisible and completely global.
Error handling in Haskell is, in practice, terrible. Not only do you constantly have to wrap and unwrap potential error values to unify different error types, you also have to deal with a dozen variants on Either and related typeclasses, none of which are used consistently across the ecosystem. And as if that wasn't bad enough, there are also actual throw-catch-style exceptions, which are not checked in anyway and, combined with laziness, can explode in your face at absolutely any time whatsoever. It's a big big mess. I stopped writing Haskell maybe a year ago, but at least back then getting GHC to print a stack trace when an exception wasn't caught was a major feat that involved recompiling every single one of your hundreds of dependencies with profiling flags. Guess how well that went most of the time.
Anyway, Go-style error handling's lack of any of these issues is what makes it attractive to me: it's simple, straightforward and get's the job done. Granted, with a bit more typing and the occasional bug that could have been avoided by not having to type the (almost) same three lines once again, but not having to jump through any of the hoops I keep encountering in more sophisticated languages is so, so worth it to me. I feel like Go stays out of my way, lets me focus more on actually getting things done than on dealing with the language's intricacies.
> Not only do you constantly have to wrap and unwrap potential error values to unify different error types
This is a legitimate issue, although in Haskell you can use one error type like Go does if you want to.
> And as if that wasn't bad enough, there are also actual throw-catch-style exceptions, which are not checked in anyway
This is also the case in Go (panics and recover).
> I stopped writing Haskell maybe a year ago, but at least back then getting GHC to print a stack trace when an exception wasn't caught was a major feat that involved recompiling every single one of your hundreds of dependencies with profiling flags.
This is also the case in Go. Errors do not retain anything about their state. You need to use a package like https://github.com/go-errors/errors, which has the exact same issue you describe with dependencies.
>This is also the case in Go (panics and recover).
It is considered good style to not let a panic escape your library API unless something just went very severely wrong that the library could not possibly continue to function normally.
>This is also the case in Go. Errors do not retain anything about their state.
Errors are an interface, you can put any state inside it you want and I've frequently done so (retain line counts in parser or network data for example)
>> Not only do you constantly have to wrap and unwrap potential error values to
>> unify different error types
>
> This is a legitimate issue, although in Haskell you can use one error type
> like Go does if you want to.
SomeException is pretty much equivalent to Go's error, but it's not commonly
returned by libraries and such so using it barely helps with the need to
convert often. In addition all the types used by libraries as error values now
need to have Exception implemented, which they don't. So, in practice, no, you
cannot. Or rather, it wouldn't solve the core problem and create a new one.
>> And as if that wasn't bad enough, there are also actual throw/catch-style
>> exceptions, which are not checked in anyway
>
> This is also the case in Go (panics and recover).
True, panic/recover exist, but in practice I find them to be more akin to
setjmp/longjmp in C than throw/catch-style exceptions in other languages. Off
the top of my head I can't recall a case where I had to recover from a panic
that I didn't create in my own code (i.e. originated in a library call).
Exceptions in Haskell and other languages on the other hand are usually all
over the place (ok, usually not in pure code in Haskell) and I feel like I have
to constantly keep them in mind to avoid letting one slip through the seams and
then later having to track down where the hell that one came from.
>> I stopped writing Haskell maybe a year ago, but at least back then getting
>> GHC to print a stack trace when an exception wasn't caught was a major feat
>> that involved recompiling every single one of your hundreds of dependencies
>> with profiling flags.
>
> This is also the case in Go. Errors do not retain anything about their state.
> You need to use a package like https://github.com/go-errors/errors, which has
> the exact same issue you describe with dependencies.
That's fair and indeed it would often be handy to know where an error came
from. But since errors are just return values I don't really expect them to
have a stack trace attached to them, just like I don't expect any other value
to have that. It would still be neat for debugging sometimes, more so for
errors than other values. I think there are some proposals for improving on
this in Go 2.
What really frustrated me about Haskell though was debugging throw/catch-style
exceptions without a stack trace, because I basically had zero clues about
where it came from and there were no clear paths to trace in the code. In Go on
the other hand, errors don't just plop up from out of nowhere and panics, which
can, print a nice stack trace when not recovered from.
Haskell is an evolving language and the error story isn't entirely resolved yet. Personally, I like the MonadThrow stategy which gives the flexibility to the caller but the IO story gets in the way.
So yes, it can be messy when using other people's code because this is very much an unstable part of the language right now. I still prefer that over the worst possible strategy (what Go picked) being effectively baked in.
Yep. For my money, the war is over, and the monad won. I shouldn't say the m-word, as it turns people off. But that's the unifying pattern.
Javascript messed it up a little bit with promises, because it's so easy to swallow errors. IMO, they should have made it so that unhandled promise rejections throw by default at the next turn of the event loop, with a method that could be called to opt out individual promises.
I don't think so. You can choose to ignore the error (I know it's bad practice), but it's allowed. And sometimes you don't know what to do with the error
Handling errors as values is great, but making that value a product type rather than a sum type is not good design. It means you have to return a result even when you have an error, and so nothing enforces that the user actually handles the error properly.
No. That is incorrect. Errors are to be handled by the programmer and every book I read says "We are ignoring errors here for the sake of simplicity, you are strongly advised not to ignore them".
Error handling is what happen after you discover there was an error.
In Go, "checking" for errors may be skipped (and people make mistake, even True Programmers), in which case they are handled just as if they were ignored.
Even if the error can be ignored for fmt.Println, it is not clear from the example whether it was even taken into account or not. The following could be required:
_ := fmt.Println("hello world")
That means that the function call can return an error, and thus you have to use the return value, but the programmer decided to ignore it. Contrast it with:
fmt.Println("hello world")
Did the programmer know there was possibly an error? What happens if there is an error (not specifically for this example, but in general), and you don't check for it? then it is ignored.
In another language, the same line could be the source of an error; but only error handling depends on the programmer, not error checking. Moreover, the default handling strategy is to abort execution. Actually ignoring errors requires more work.
Most linters that concern themselves with errors will complain about this and I would not accept any code that does this beyond tests.
Go allows you to do certain things but it's not a fallacy because it's something that's allowed but not necessarily tolerated by what most consider "good Go coding standards"
My consulting experience has shown that beyond startups, or a few Fortune 500 companies like Google and FB, linters and code reviews are seldom done, unless we are speaking about safety critical domains like aeronautics, autos, train control systems,medical devices....
So because a lot of people don't use best practises that means the language in which these are to be used is worse off?
It strikes me a bit like saying "because most people don't wear seatbelts, cars are unsafe because you may get ejected out of the cockpit in a crash". Wear a seatbelt and use linters, I'm not going to excuse the malpractices of the larger industry.
Optional external tools, are exactly that, optional.
Lint was created for C in 1979, a minority used it or its commercial versions like Gimpel, until clang was introduced with static analysis built into the compiler.
Anything that isn't enforced by the language, tends to be ignored, hence why we need good defaults that make workarounds a big pain.
More safe defaults also means I have to think more about escape hatches. Sometimes ignoring an error is the intended way of operation, although rare. Same goes for some other parts of the go coding standards. Sometimes you need to violate them and having to first use some escape hatch like Rust's unsafe is annoying.
Not really. It has changed my coding style so I now only add variables when needed, even in languages like JS. I only get the "unused variable" error about once a month.
It's more like every 5 lines. I love it as every error is handled correctly and in scope. No error is left behind. Most languages I've used devolve into the GLOBAL MASTER DEFAULT HANDLE ALL ERRORS which is just gross.