Every go developer I know who likes the language disagrees with this sentiment (including myself). If you really want async/await Go is probably not the language for you. Thats ok! There are a lot of languages that other people like that I do not want to use.
We don't need to have one language to rule them all. Its fine for languages to make different choices and trade-offs.
Nowhere did I suggest one language should rule them all. Golang has advantages like efficiency and greenthreading, but that's not because they omitted async/await and exceptions. I just think that was a mistake. And I do use Golang on my team.
Having used both async/await and goroutines, I much prefer the latter. But yes, goroutines and channels are powerful tools that require understanding and discipline.
I used languages with exceptions for 16-17 or so years and I've been using Go for 7-8 years.
It took me quite a while (years) to go from thinking that returning error (conventionally as the last return value) was a bit clunky to thinking that this is actually a very good idea. I don't mind the if err != nil pattern. I can't think of a single experienced Go programmer that is sufficiently annoyed by this to complain. I sometimes see people new to the language complain about this, but it tends to fade as they adapt.
With the ability to join errors, do sensible comparisons etc, errors work well for me when needing to express high level errors and their low level root causes. (eg the high level error may be "failed to read configuration" and the root cause might be "permission denied when opening file X").
I've come to realize that I like Go style errors better than exceptions for two reasons. First off, a lot of code in, for instance, Java uses unchecked exceptions. Which means that the whole idea that you have to handle exceptions goes out the window.
This, of course, varies from codebase to codebase, but the it is common enough that having exceptions available in a language doesn't mean people will use them in a way that forces discipline. Which kind of makes exceptions pointless. They just become syntax for emitting and handling errors. And awkward syntax at that. Which is the second reason. Exceptions tend to introduce extra scopes you have to deal with. This is annoying and in some cases it can get somewhat complicated.
Exceptions, the way they are commonly used, are a weak enforcement mechanism made more dangerous by people pretending it isn't. If you want discipline, neither exceptions nor Go errors are the way to go. You probably want to look to something like Rust.
I have a hard time seeing how exceptions would make Go better. Yes, if someone could come up with some syntactic sugar for Go 2 to deal with errors, that would be nice, but I have seen no proposals for this that represent actual improvement.
`if err != nil` is actually extremely readable and to justify new syntax for expressing this it would have to be a _lot_ better. I'm open to listening if people can come up with something.
Exceptions didn't end up doing what people hoped it would do in Java. We should learn from that and try not to pointlessly repeat this mistake. Besides, Go has already made its choice. Retrofitting it is only going to do harm.
The entire difference is whether or not you have to type "if err != nil { return err }" repeatedly. Some languages force you to handle exceptions, and some force you to handle non-exception errors.
Colored functions are a huge hit to take, and the things you do to make colored functions tolerable are also things you can do to mask off the complexity of channels from library callers. Async/await for Go seems like it would not be a win.
I think exceptions are pretty much a dead letter in new language designs at this point? You could get people on board for real algebraic types and matching.
Since the boat has already sailed for Golang having async/await from the start, adding it now would require maintaining compatibility with uncolored functions. Waiting for a channel to have data from a go func is similar. JS has similar compatibility with uncolored code using Promises. Python's migration to async/await is a lot worse because it didn't have coherent parallelism features to begin with.
Exceptions are a smaller deal than async/await. Basically instead of manually writing "if err" everywhere, it's implicit if you aren't catching it. The most popular languages do this. Rust "?" syntax is also a decent compromise.
Yes, but when writing idiomatic Go one would prefer error types over using panic (panic is Go's term for an exception).
The parent is really complaining about idiomatic Go. Which is fair, because idiomatic Go feels like writing Java 6 (that is to say, making lots of compromises due to limitations of the language).
No. An exception is a datatype. Not completely unlike an error type in concept, but intended for erroneous conditions that could have theoretically been caught at compile time (i.e. the programmer screwed up), as opposed to conditions that are not decidable at compile time (i.e. something happened in the outside world).
In Go, panic creates an exception, which may be the source of your confusion. The exception contains any metadata you passed to panic along with other data, like a stack trace. This is not panic in and of itself, though.
> but when writing idiomatic Go one would prefer error types over using panic
Not really. Idiomatic Go says that errors are in no way special and is faulty to think of them as being special. They are just values like any other. Go exceptions allow attaching any value as metadata.
The official line is that panic stack traversal should not cross package boundaries, but internal use is perfectly acceptable. Even the Go standard library does it.
Thank you for taking the time to reply so thoughtfully!
So, if I'm understanding you correctly:
An exception is an unexpected failure at runtime that could have been caught by the programmer. For example, you might have a switch block with a default block saying "this can never happen". The programmer might miss a case in the switch block leading to an exception. This would be analogous to an unchecked exception (RuntimeException) in Java -- these exceptions are not required to be caught.
In Go, `panic` creates an instance of the type `exception`. When `panic` is executed it attaches metadata like the stack trace to the created exception. When I say "exception" this is the behavior that most people think about.
An error is for an expected failure, like a file potentially not existing when performing I/O operations. Like you said (and this matches my understanding), error types are just a normal type -- really any type that implements the `Error` interface. There are a lot of utilities functions/libraries that act on that `Error` interface, like assertions for unit tests, the multierr library, etc.
`Error` is similar to a checked exception (Exception) in Java, though it misses the behavior you expect like requiring it to be caught, and the try-catch syntax. Additionally, you don't have the stack trace/metadata automatically added.
Go programmers seem to _heavily_ prefer errors over exceptions, even when they should be using `panic`.
My problem with errors in Go are that they are too easy to ignore accidentally and they don't contain stack traces, so they can be hard to track down. I inherited a codebase where we had _hundreds_ of unchecked errors being silently ignored.
I understand that these problems go away if you have linters or a very detail-oriented team, but I unfortunately have no power over the actions of my team before I join. It's also _very_ hard to convince a team that they've been doing things incorrectly by ignoring errors.
> My problem with errors in Go are that they are too easy to ignore accidentally
This is a problem for values of all types, not just errors. One I'm not sure we've figured out how to solve[1]. There are a few languages out there that force variable assignment to try and address the problem, but even then there is really nothing to say that you haven't accidentally ignored the variable assigned.
> It's also _very_ hard to convince a team that they've been doing things incorrectly by ignoring errors.
To be fair, if you are able to completely leave out entire blocks of logic without anyone noticing even under the most cursory of testing, perhaps it wasn't actually needed? Forgetting entire code branches isn't exactly a subtle bug.
It's a problem for values of all types, which is why exceptions are usually handled specially in other languages. I get that it seems impure, but everything you call has some way to fail <1% of the time that you probably want to handle very differently from any other outcome. It's sensible for a language to force you to either designate a different code path for that or let it bubble up (edit: which, to clarify, Golang does not enforce with error return types, which is what the other person dislikes about it).
I've read a lot of code in my life. I see that exception handlers are sometimes used to carry errors, virtually always exceptions, but almost never anything else. Who is it that you are think are passing around email addresses and geocooreinates using exception handlers?
But even assuming it is done sometimes, is the developer going to actually handle it? I don't know how many times I've come across "catch" blocks that are empty or something equally inappropriate. Nothing was gained. It turns out that programmers will still forget no matter how hard you try to hold their hand.
I'm not sure there is any other solution than to test it, and once you get into testing, entire blocks of logic missing are going to stick out like a sore thumb. Forgetting an entire branch is not exactly a subtle bug. At that point it really doesn't matter what language constructs you do or don't have available.
> I don't know how many times I've come across "catch" blocks that are empty or something equally inappropriate. Nothing was gained. It turns out that programmers will still forget no matter how hard you try to hold their hand.
You are right, but the difference is that the programmer is making the choice to ignore that error versus accidentally ignoring the error.
There was no indication that it was an active choice. I'm speaking to where it was clearly incorrect behaviour, not where one honestly wanted to ignore the condition. I'm assuming these programmers would write out the boilerplate (perhaps even automatically by some IDE feature) and then forgot to return to it to fill in the logic.
And fair enough. It would be just as easy to forget to do that as it would be to forget to handle errors in any other language. There is no silver bullet here.
> This is a problem for values of all types, not just errors. One I'm not sure we've figured out how to solve[1]. There are a few languages out there that force variable assignment to try and address the problem, but even then there is really nothing to say that you haven't accidentally ignored the variable assigned.
The difference is that the happy case is well-tested. If you aren't handled the error-free route you probably will notice that very quickly unless you aren't testing your code, even manually.
e.g. it doesn't matter if you ignore normal variable assignments because the programmer will usually catch that themselves. They will not likely catch all of the possible error assignments without some help.
> To be fair, if you are able to completely leave out entire blocks of logic without anyone noticing even under the most cursory of testing, perhaps it wasn't actually needed? Forgetting entire code branches isn't exactly a subtle bug.
In this case we were silently ignoring errors which caused multiple types of issues that we had to then manually track down. This is an entire class of defects that can be avoided with better tooling, either at the language level with checked exceptions, or with a linter like errcheck.
This is confusing, so I'll put it this way, Golang doesn't have exceptions the way someone coming from Javascript, Java, Python, ObjC... would understand: thrown anywhere and automatically propagated up the call stack unless caught.
Like randomdata said, Go does have this with panic/recover, it's just that Go programmers (from what I have seen) greatly prefer Error types which do not automatically propagate up the stack until caught.
Go _doesn't_ have any form of checked exceptions, though, which are required by the compiler to be caught by the caller.
A minor nitpick, but checked exceptions are a feature entirely unique to Java (maybe also one or two JVM languages?), so it really shouldn't be discussed so often when touching this topic.
That's fair! My point is that being forced to check exceptions can be beneficial. Go does not have any mechanism for this.
If you want to use a Result/Optional type I think that is way better than exceptions. As long as the compiler can enforce that you are acknowledging (or explicitly ignoring) both the happy and error cases. Ideally the error case for the Result/Optional type would also have an attached stack trace.
Panic/recover is perfectly equivalent to try/catch in most languages. There is some small difference related to exactly how un caught panics are handled (in Java, an un caught exception kills the thread that raised it; in Go, it kills the whole Go process); and the amount of built-in type checking for recover vs catch (but you can do manual typechecking and re-panic from a recover if you want). But otherwise they are almost perfectly equivalent.
Of course, Go code uses panics much more sparingly than Java or C# or JS or Python use exceptions.
It's different syntax, less convenient, and less obvious what you're try-catching. Also you can't scope it to just part of the function.
func outer() {
defer catch() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}()
// ...
doSomething() // Can panic
doSomethingElse() // Can also panic
}
But in any real Golang situation, you'll be working with libraries or teammates who return (type, error) instead of using panics. You might say this isn't inherent to the language, but it kinda is when all the built-in standard libraries like net/http work this way and the official language style guide tells you to do this.
> Also you can't scope it to just part of the function.
Why not?
func outer() {
func() {
defer catch() {
if r := recover(); r != nil {
fmt.Println("Recovered from doSomething:", r)
}
}()
doSomething() // Can panic
}()
func() {
defer catch() {
if r := recover(); r != nil {
fmt.Println("Recovered from doSomethingElse:", r)
}
}()
doSomethingElse() // Can also panic
}()
}
I don't get why we keep getting these "expert" comments from people who have never used Go before.
Hell, if you long for try/catch for some reason, you can even get creative:
No, I think you just showed that trying to use Go as if it was designed to use panic as an exception is ugly. Other languages use exceptions perfectly well and I would say consequently have a much better error handling story than go.
I really like go - a lot. But its error handling is truly awful.
Try-catch is awful if you're forcing it in a language that doesn't really support it. I provided a JS try-catch example above; is there something wrong with it?
The recover is still function-wide in your example, which is what I said. You can nest funcs just to deal with this, but it's ugly, and code reviewers won't like it. I use Go on my team, and yeah I'm not as expert as the team who created Go here, but idk why you keep saying I've never used it. My complaint about "if err" is pretty common among Go users, who would understand the joke of calling it "Errlang."
> I provided a JS try-catch example above; is there something wrong with it?
Yes, it suffers all the same problems. And then doesn't even help you with the errors once you get them! You then have to resort to this kind of craziness to do something with the error:
Which leaves you wondering why you didn't just write:
err := doSomething()
switch {
case errors.Is(err, ErrFoo):
fmt.Println("foo")
case errors.Is(err, ErrBar):
fmt.Println("bar")
case errors.Is(err, ErrBaz):
fmt.Println("baz")
default:
fmt.Println("unknown")
}
At least Java gives you multiple catch blocks to make things slightly more sane.
But I get the impression that those who find benefit in passing errors using exception handlers don't handle errors. If you don't have to worry about errors in the code you write, then I think there is a good case to be made that errors as exceptions is a better approach.
That is, after all, the difference between scripts and systems. Scripts can simply fail, leaving the user to try again. Errors as exceptions, or something in the same vein, make sense as the predominant mechanism in scripting languages. Systems, on the other, have to deal with failure. You don't get to just bubble it up and let the user deal with it. This is where errors as exceptions becomes a nightmare. Go is unabashedly a systems language. It is not meant to be a scripting language.
The point of exceptions is to make it convenient to propagate errors upwards and unlikely that you accidentally ignore an error, as discussed in the other thread. This doesn't magically do your error handling too, but the Golang example isn't any nicer.
They aren't different jobs, though. The two most common uses of JS are backend systems (like you'd often use Golang for) and web frontends, not scripts. Backends will usually catch errors in one place, an HTTP or similar handler, sending back the appropriate status code with maybe a payload. Golang backends do something similar, which is why you see so much "if err != nil... return err" in practice.
Python is more for scripts aka CLIs, but Python backends are fairly common too. And Golang CLIs are also common.
This Golang guru talk of "handling errors" is almost entirely BS in my experience. I've almost never seen a Go library or code example that does anything other than bubble an error it got from its lower levels up to its callers, with some extra context if you're lucky. The rare exceptions are either some retries for network operations, or just ignoring the error and returning some default value.
I hear this in C++ too since we can't use exceptions here. "Unlike some other toy languages, we don't ignore errors, we handle them head-on." Uh no you don't, in fact it took a lot to make people stop ignoring errors or "handling" errors by crashing the entire service.
But at least we have a rule checking that you actually did something with the return statuses.
Well ok, you can draw the similarity in how they both automatically bubble up. But the catching behavior is different, and they look different. Overall they weren't designed for similar uses.
There is nothing to be confused about. The syntax may be slightly different to other languages, but there is no meaningful difference in the design. If you understand exceptions and exception handlers in those other languages, you understand them in Go. There is no exactly a lot you can do with exceptions to see them differ in any big way.
Why is it that Go attract so many "experts" who have clearly never used the language even just once?
The language doesn't impose this upon you. You can use the exception handling system to pass values around, just like you might in the aforementioned languages. Even Go's standard library does it. Read the encoding/json source sometime.
You might impose it upon yourself when you start to understand the pitfalls of using exceptional handlers for anything other than exceptions, but such is engineering. Everything comes with tradeoffs.
One technical challenge to using panic to represent normal exceptions is that most go code is then not exception safe. This was a design choice for the ecosystem if not the language itself (see eg: Effective go about panic not crossing package boundaries, or Google's style guide).
Within a package it can work, but then you'll find language support lacking. For example defer is scoped to a function so you need to write pretty unusual code to make the equivalent of a try block.
What's imposed on a Golang user is two choices, panic/recover or regular error handling, and almost always you take the latter. There's no option of using exceptions the same way you would in most other languages.