and (2) a way to cut down on if err != nil { ... }
boilerplate that pervades all Go code.
Am I weird in liking the explicit error handling? :/ and (2) a way to cut down on if err != nil { ... }
boilerplate that pervades all Go code.
Am I weird in liking the explicit error handling? :/ result, err := foo()
if err != nil {
return nil, fmt.Errorf("Error message: %w", err)
}
This pattern is just so commonplace, but it's four times as long.Contrast with Rust, which handles errors as `foo()?` or `foo().context("Error message")?`*. It's still explicit — I like it better than silently-propagating exceptions — but my functions don't wind up being 75% error handling.
* Where the `context` function comes from an error-handling library.
1) The aforementioned boilerplate code that makes up a significant chunk of many Go projects and adds no value.
2) With explicit error handling, however deep your call stack is, you are relying on everything in that stack to have done the right thing with regards to error handling. With exceptions, you have to go out of your way to screw them up.
I can't tell you how many times I've gotten dumb error messages like "Invalid string" from a 3rd party library that take forever to debug, and accidentally swallowing an error is even worse. A simple (even crappy) exception message + a stack trace is much easier for me to use for debugging than even the best handcrafted error message in 99% of cases.
I write somewhat equivalent amounts of Python, Java, and Go lately, and each one has their good and bad parts, but there are few things I dislike as much as Go's error handling patterns.
I don't think I agree with this argument. In many languages, there's no obvious indicator that any given function may or may not raise / throw an exception. And even if it doesn't throw an exception today, it might tomorrow, and it's easy to forget to update all callers.
Since Go makes errors a return value you have to actively discard the result (by replacing it with a underscore, e.g. `res, _ := getResults()`) or take the time to handle the error.
And just like an intermediary library in Go can swallow the error, so can an intermediary library in an exception language catch and discard an exception.
It seems to me that the result is more errors are properly handled in Go - because they are explicit - while uncaught exceptions often cause bugs that make it to production.
For context, I recently switched jobs from one where I wrote Python for 4.5 years to a job where I've been writing Go for about 5 months.
Right, but because `err` ends up getting re-used in so many cases, you can re-assign it and forget to do anything about it (which is what I've found in most cases where an error was inappropriately suppressed in Go).
In most cases, simply bubbling up the error to something that will generically handle all errors is the right thing to do, and in the case of exceptions, even if you don't know about one, that is what will happen. I just sampled a handful of the top Golang repos on Github, and found very few cases of anything more than the standard `if err != nil; return nil, err' pattern that just bubbles the error up.
The alternative is Zig or Rust's thing, where you still have explicit error handling but you don't repeat the same four lines all the time. This is maybe helpful because in Go one has to read those lines carefully to see if anything unexpected is happening.
LoadData(&data)
Does this function return nothing or an error ?
https://play.golang.org/p/k-eTs3dC_wK
Note that Go playground also runs go vet, a static analysis tool.
This is a bad thing because it’s not narrow. It’s like a language’s “catch” statement not allowing you to catch specific exceptions. Once someone has decided to throw away an error with the underscore assignment, all future errors the underlying might return are swallowed as well.
In other words, it takes even more boilerplate to have narrower error exemptions than to have the generic exemption.
> It seems to me that the result is more errors are properly handled in Go - because they are explicit - while uncaught exceptions often cause bugs that make it to production.
An error in go that is blindly returned all of the way up the stack has no difference with an uncaught exception.
- the code has failed and the developer needs to be informed what to fix
- the environment has failed and the user needs to be informed
Exceptions are for the former, which Go still supports via Panic(). Errors are for the latter. Which is why all the boilerplate Err code creeps in.
I don’t like getting stack traces from applications when I’m a user. It always feels to me like the application is only half finished. When the error is environmental (eg file system permissions) a stack trace just muddies the water with unnecessary output.
This is why I like Go’s distinction between errors and exceptions.
All this was figured out decades ago, that's why exception systems were invented, but language designers keep making the same mistake over and over again in an effort to simplify the unsimplifiable.
Exceptions do exist in Go, it’s called “Panic”. This whole “Go doesn’t have exceptions” meme is completely untrue. It’s just not the preferred way of error handling because, unsurprisingly, most application errors are going to be environmental. But if you select an out of bounds index in a slice or don’t handle a nil pointer correctly you’d get a panic — because those are clearly developer issues that warrant a stack trace rather than environmental problems that need a friendly user facing error message.
Whereas exceptions fail safe, ignoring the error means not catching it, which means if the error does occur, it will load up the debugger.
This is the whole Java "checked exceptions" misfeature again.
The main benefit of Go is that the people that defined the library ecosystem actually decided to handle errors. It's the bare minimum, but it's better than just forgetting that the error exists and not documenting it either.
The language is ... not the best, but the libraries tend to be more robust.
What language are you thinking of when saying Go libraries do better at error handling?
stream.map(f)
doesn’t allow f to declare any checked exceptions. We could catch and wrap everything but that obscures the useful code without any improvement in safety.In SaaS, mobile, and appliances, the environment is entirely or almost entirely controlled by the programmer - a missing file is the developer's problem, and the user can't do anything about it.
In enterprise software, there is a third entity, the Administrator, that has (almost) full control over the environment, and usually there are layers of 3rd party software that control it directly. A missing file or permission error is useless to the user, most likely it needs to be logged somewhere that Admins check, and the user simply told to contact their Admin.
Finally, even in personal desktop software, the environment is jointly owned by the user and the developer - the developer is normally responsible for setting up the initial environment through some kind of installer (msi, deb, make configure etc.), and many environment issues are bugs in the installer, not user errors.
Of course, the same library may very well be used in all of these vastly disparate deployment scenarios - the library can't decide what is the source of an error, so making it arbitrarily decide between two possible kinds of errors is wrong.
Note that even the reverse is not clear; an index out of bounds error could be a user problem - if they are trying to access the 7th element of a 5 element list. The fact that you'd normally do
if userSelectedElement > len(arr) {
return nil, fmt.Errorf("Array index out of bounds")
}
return arr[userSelectedElement], nil
Is only because of the convention you were proposing - `return arr[userSelectedElement]` does the same thing alone, if we disregard the convention (in a memory safe language, of course!).Trust me, searching through a stack trace on a centralised logging system isn’t fun.
> Finally, even in personal desktop software, the environment is jointly owned by the user and the developer - the developer is normally responsible for setting up the initial environment through some kind of installer (msi, deb, make configure etc.), and many environment issues are bugs in the installer, not user errors.
Desktop Linux, yeah. It’s seldom that simple in servers though. SELinux, custom config, custom iptables rules, network wide UIDs (eg shared storage volumes), there’s so much that can go wrong the moment you do enterprise.
> Note that even the reverse is not clear; an index out of bounds error could be a user problem - if they are trying to access the 7th element of a 5 element list. The fact that you'd normally do
If you’re writing software that doesn’t do input validation and bounds check then you’re a failure of a developer. Sorry but this is the bare minimum I’d expect a developer to do.
A stack trace is still better than a one line error with no context. It's a kind of 80% solution - it's not ideal (a perfect error includes only the relevant context), but getting it's much better for 0 effort than error codes/values give you for free.
> If you’re writing software that doesn’t do input validation and bounds check then you’re a failure of a developer. Sorry but this is the bare minimum I’d expect a developer to do.
So what is the profound difference between doing input validation in your own code vs letting the array accessor do it?
Yeah, a stack trace is better then an “undefined error” type message. But the point of forcing error messages over exceptions is you’re enabling you’re developers to write meaningful error messages. So your point is moot.
> So what is the profound difference between doing input validation in your own code vs letting the array accessor do it?
- Meaningful error messages (eg has the code failed because of a bug or because of invalid user input?),
- security hardening,
- reducing potential undefined behaviours,
- thorough unit testing,
- self documenting code (the code clearly defines what the happy path is and when it’s possible for a user to break out from that)
Etc
The panics are there to be used only in cases where the further code execution is impossible. Not to inform the developer about an error.
Errors are a general solution to report an unexpected event during execution. The destination of such report depends on the part of code the program failed.
Personally my one gripe with Gos errors is that EOF is handled as an error.
Not sure I got this part. If you want to stop the program and don't to print the usual panic messages into the console - you can log\print whatever you want and call os.Exit().
At the same time you can log stack trace without panicing.
>Or at least I think it does.
Maybe, it's just that "the code has failed and the developer needs to be informed what to fix" applies to errors just a much as to panics. The only difference is that an error is the situation during the execution where you can continue the job and a panic requires execution to stop (in most cases).
At the same time panic are 'developers only'.
I remember well java codebases littered with:
try { .. } catch() { // Todo }
Usually cheaply inserted by the IDE.I'm pretty sure nowadays there are linters that will ensure you have to do some extra work to actually check in such code, but still...
But either way, you have to go out of your way to do that, which is sort of my point.
The one place where you do see dumb boilerplate and chances to screw up is in dealing with checked exceptions, which have been controversial since the very beginning. I think they are one of those things that seem like a great idea in theory, but end up not working out so well in practice, but (like people who appreciate Go's error handling model) there are people who disagree.
However it is very easy to do the wrong thing using Golang. Just one random library doing `fmt.Errorf` without the `%w` verb is sufficient to lost all information from there on. I would much prefer some kinda of annotation that does the correct thing by default (wrapping the error) instead of the "every error should be explicitly" approach of Golang.
yeah this indeed happens with checked exception and careless developers just wanting to get stuff to compile (we'll handle that property later) and the IDE helpfully providing the boilerplate that resolves the compilation error (and assumes you'll fill the body)
What saddens me is that these kinds of matters will never be fixed in Go. Go has a stubborn anti-feature mentality and while it does help preventing feature creep, it overall harms the language in the long run. For a language created and maintained by Google this is a huge missed opportunity.
The stability of Go is something I value a lot, so I don’t mind stop-gaps like this too much on balance.
I was curious about this and it’s true, I didn’t know these functions could err, TIL.
The Godoc states:
> It is conventional not to worry about any error returned by Printf
The vast majority of errors only stop or rollback the current action. An incredibly small amount of code uses errors to detect stop conditions or to retry.
The issue is not explicit error handling it’s specifically Go’s, which is verbose and half-assed. Not entirely unlike java’s checked exceptions though unlike checked exceptions we have plenty of other (and I’d argue better) implementations of “explicit error handling”.
Go’s error handling is a relatively minor improvement on C’s, but we’ve gotten quite a ways beyond that since.
val, err := int_or_err(foo)
if err != nil { return nil, err}
return val.add(bar), nil
Vs result := int_or_err(foo)
return val.map(add,bar)
I'd even take some sort of result := int_or_err(foo)
return val.map(add,bar).tuple()
to return a val,err pair in order to match conventional go.Consistency is more important than conciseness. Clear is better than clever. Plus, a function invocation / .map() is heavier than an err != nil check / condition.
Don't get me wrong, I appreciate the Either pattern as an alternative to if err != nil, but I also appreciate the really dumb and straightforward approach of non-clever Go.
However, Go's `if err != nil` is not more explicit than, for example, Rust's question mark operator. The former is more verbose than the latter, yes. But both are explicit in that we can know which line can return an error.
The `if err != nil` is probably okay if there are only a few lines that can return an error, but if most lines in a function can return an error, it will result in too much noise. A real world example where panic is abused as an exception-like approach because the "proper" error handling using `if err != nil` is way too verbose: https://pkg.go.dev/github.com/apple/foundationdb/bindings/go... (Actually Go's stdlib too sometimes abuses panic in a similar way.)
You just need to have some thought when writing your code:
let a;
try {
a = someFailingFunc();
} catch (e) {
// handle
}
// use a
// do work func doRequests(url1, url2 string) resp throws HttpException {
resp1 := http.DoRequest("POST", url1)
resp2 := http.DoRequest("GET", url2 + resp1.ID)
return resp2
}
vs func doRequests(url1, url2 string) (resp, error) {
resp1, err := http.DoRequest("POST", url1)
if err != nil {
return nil, fmt.Errorf("Error in req1: %w", err)
}
resp2, err := http.DoRequest("GET", url2 + resp1.ID)
if err != nil {
return nil, fmt.Errorf("Error in req2: %w", err)
}
return resp2, nil
}
Of course, we can write the second example with try/catch as well, but the whole point of exceptions is to be able to write the first one when appropriate.Of course. I'm talking about when you actually handle your exceptions you should be specific about the lines you're handling.
There is no standard solution though, so you have to rely on 3rd party libraries or use platform specific solutions.
I think people's issue is that there's no "I don't care about errors, just show me a stack trace" option like you get with exceptions, or more or less with Rust's `?` if you don't use `.context()`.
I really liked their `check` proposal. I hope they will bring it back to life.
I have marginally more experience with Rust (not that much of either) which instead gives Result types, tuples of 'ok' and error values. And, crucially for this thread, they must be explicitly consumed.
That enforced error handling is novel to Rust afaik, and after a bit of getting used to, an excellent feature I think.
But I'm not sure that the idiomatic (because that's all it is really, rather than language feature?) Go is materially different from deciding all your Python functions will return Tuple[TOk, TErr] and sticking to it, or even that different from returning ok types only and raising exceptions, really.
Hardly, though Rust of course has merit in making it popular, to the point that people think it was introduced by the language.
Well that's not true, that's completely orthogonal?
There's a go vet pass as well. https://pkg.go.dev/github.com/golangci/govet#hdr-Unused_resu...
Scala's Either[A, B] is another example.
Is Scala's Try[A] "specific"?
Whereas if you compare Rust to Python, C(++), or Go as I was - having to consume a returned 'result' is more notable.