Far easier to work with and understand when you dont need to perform a massive disruptive ceremony to handle exceptions. I've been working full time in java for the past 4 years and basically no one handles exceptions because its so cumbersome and bad to read. Not so in Go. Go does it better.
> Far easier to work with and understand when you dont need to perform a massive disruptive ceremony to handle exceptions. I've been working full time in java for the past 4 years and basically no one handles exceptions because its so cumbersome and bad to read. Not so in Go. Go does it better.
No Go doesn't do it better, Go just doesn't give you the choice, from a convention perspective. You have a discipline issue with Java, it is not an issue in the language itself, it's with you and your team.
Ironically I've seen a few Go libs and project trying to re-invent optional types due to the verbosity of Go errors, just like people were trying to roll their own generics with interface {} or code generation before they were added to the core. It's a demonstration that some developers don't like the status quo.
The encoding/json package in the Go standard library uses exceptions for error handling. You absolutely have a choice, but it's a choice you're not likely to want to make because exceptions are named exceptions for good reason: It's for exceptions, not errors. Those are very different things.
But sometimes the trade-offs of overloading something beyond its intent is worth it. In those cases do it. Balancing trade-offs is what engineering is all about.
> No Go doesn't do it better, Go just doesn't give you the choice, from a convention perspective. You have a discipline issue with Java, it is not an issue in the language itself, it's with you and your team.
Its my entire company, and its with the open source ecosystem. Its naive to think that language constructs dont influence dev's decisions. You need to make it easy to do the right thing, not hard, and go makes it easy, which java fails at.
The right thing is to automatically maintain error context (e.g. stack traces) which golang fails at. Furthermore, the vast majority of the time you will have a top level handler that will log the error anyway. golang is just introducing boilerplate for the sake of it, with none of the upsides. Not to mention it also makes it easy to accidentally ignore or overwrite errors, I've seen several instance of that happening over several years of working in golang. Something that would never have happened in an exception based language.
It's quite trivial to write a generic function that handles a particular exception else returns a default value, which addresses the above scenario.
final var x = getOrDefault(C::foo, Something.class, quux());
return x.bar();
And now with pattern matching in Java, it's trivial to write something similar to Rust's/Scala's `Result<T, E>`/`Try<T>` types and be explicit about all exceptions.
I haven't compiled the following, but it's along the lines of:
final var x = switch (foo()) {
case Ok(var r) -> r;
case Error e -> quux();
}
return x.bar();
Neither of those is equivalent to what I wrote (in different ways!), and the first one doesn't even work because of type erasure - you can't write generic functions over exception types. You're also focused way too much on the specific lines I wrote and not the general pattern that leads to exception over-handling.
> trivial... trivial
Rethink your use of this word.
Anyway, alternative proposals don't really matter - I could also offer patterns for Go which make accidentally ignoring error values more difficult. We're talking about actually extant code, and nobody writes Java (or Go) like that outside of forum arguments.
That you've marked the function `throws E` is an admission it doesn't really work; that's the one exception type it definitely doesn't throw. (Default handling is also still different than the starting point.)
Please just stop trying to show off how much you know when I'm trying to discuss how programs in general are actually implemented. You're very clever, but part of why you keep getting it slightly wrong is because probably you don't even write error handling like this day to day.
> That you've marked the function `throws E` is an admission it doesn't really work
Correct, I didn't spend a lot of time refining it. I suppose the only way to make it work is to remove `throws E` from the signature and make it `throw new RuntimeException(e)`, and be similar to C#/Kotlin/Scala.
My last JVM job used Scala, and we wrote things that are similar to
val x = foo() match {
case Success(res) => res
case Error(e) => return quux() // Returns from function scope
}
> Its my entire company, and its with the open source ecosystem. Its naive to think that language constructs dont influence dev's decisions. You need to make it easy to do the right thing, not hard, and go makes it easy, which java fails at.
Go makes it pretty easy to ignore errors, it's even worse than Java. An uncaught managed exception will not compile, and an uncaught unmanaged exception might terminate your program.
_ := YieldsAnError()
will will never terminate your program. Go makes it pretty easier to do the wrong thing.
> An uncaught managed exception will not compile, and an uncaught unmanaged exception might terminate your program.
While it's nice that uncaught exceptions stop the program from continuing with invalid state, it'd be strictly better to not have them in the first place. That's what languages with explicit error returns provide - can't have uncaught errors if you're forced to explicitly deal with every possible error. The resulting software is way more robust that way.
> _ := YieldsAnError()
This is a deliberate decision to ignore the error - the programmer needed to explicitly type this out. As such, it's not a problem at all. Even if it was, writing a linter to detect it would be trivial.
The real issue with Go's error handling is that you are allowed to implicitly ignore errors in certain cases. Most of them are caught by linters, but some edge cases remain and not compiling is better than needing a linter in the first place.
> This is a deliberate decision to ignore the error - the programmer needed to explicitly type this out. As such, it's not a problem at all. Even if it was, writing a linter to detect it would be trivial.
So is not handling Java exceptions is deliberate, and again, Java forces you to handle managed exceptions.
> While it's nice that uncaught exceptions stop the program from continuing with invalid state, it'd be strictly better to not have them in the first place. That's what languages with explicit error returns provide - can't have uncaught errors if you're forced to explicitly deal with every possible error. The resulting software is way more robust that way.
Errors as value in Go are purely a convention, nothing precludes you from doing the exact same thing in Java, or JavaScript or any other possible language that has exceptions. Furthermore, Go has (stupid) exceptions, it's called panics.
> So is not handling Java exceptions is deliberate
That's the thing, there's no visible difference between a deliberately ignored exception and a purposefully ignored one. The code looks exactly the same:
foo()
Did I purposefully ignore the exception or not?
Can `foo()` even throw an exception?
There's no way to know.
Once you've programmed with explicit error returns, coming back to exceptions is unnerving. Anything may or may not throw at any time and there's no way to know.
> Java forces you to handle managed exceptions.
I'm referring to the far more common unchecked exceptions. Java's checked exceptions are a whole another bag of demons I don't want to get into right now.
> Errors as value in Go are purely a convention, nothing precludes you from doing the exact same thing in Java, or JavaScript or any other possible language that has exceptions.
Thing is, conventions matter. A lot. The proof is in the pudding:
- what's the ratio of exception-based Java code to error-based Java code?
- what's the ratio of error-based Go code to exception-based Go code?
Somehow both ratios end up close to 100%, despite neither language forcing their users to use a specific error handling method.
That's because there's space between forcing something and completely ignoring it. I often call it "nudging". Languages often do, and should, nudge their programmers into certain ways of programming that fit their philosophy.
> Furthermore, Go has (stupid) exceptions, it's called panics.
I assume by "stupid" you mean that panics are underfeatured compared to "real" exceptions. That's intended, you're not supposed to catch panics in the first place. They're only there to address runtime issues that are almost always unrecoverable: running out of memory, out-of-bounds array access, regex compilation failure, etc.
You're arguing a technicality, which is a waste of time. We should focus on reality, not imaginary theoretical issues. In reality, that never happens. And people even develop linters (frankly unnecessary) to guarantee it, if you're really paranoid.
This is a social problem, influenced by language design. You need to be thinking about the way humans actually behave in practice, not how they theoretically might behave.
In practice people do not accidentally skip errors in Go. In practice people do not handle errors at all in Java. This is the responsibility of the respective cultures and language designs.
> You're arguing a technicality, which is a waste of time. We should focus on reality, not imaginary theoretical issues. In reality, that never happens. And people even develop linters (frankly unnecessary) to guarantee it, if you're really paranoid.
Because you aren't? you're projecting.
> This is a social problem, influenced by language design. You need to be thinking about the way humans actually behave in practice, not how they theoretically might behave.
Sure, your problem with Java is also a social issue that has nothing to do with the language.
> In practice people do not accidentally skip errors in Go. In practice people do not handle errors at all in Java. This is the responsibility of the respective cultures and language designs.
This isn't Java's "culture" to not handle errors at all. Go's conventions imposed in its std libs are certainly not a superior model by any measure as I previously showed.
Edit: Answer to your follow up since HN ratelimits flamewars
> This is java's culture, and I assert it from experience. It is a result of the language design (and communiques from language authorities).
Your appeal to authority is a logical fallacy, you have never demonstrated that it is Java culture. I say it isn't Java culture the same way you establish an unfounded claim.
> You only showed that it was possible to misbehave in Go, which is totally uninteresting. You have failed to show that your theoretical problem is a practical problem. This is a perfectionist argument.
You don't get to say what argument is deemed interest and which isn't in a discussion.
Ignoring Go errors is as much as a practical problem that not handling Java exceptions. The difference being that Java does force the developer to catch managed exceptions, Go doesn't care, it's purely a convention. A language construct to deal with errors baked into a language is in theory and practically superior to random conventions established by a vendor. PERIOD.
Whatever experience you had with Java isn't representative of Java's culture at all.
>Sure, your problem with Java is also a social issue that has nothing to do with the language.
Quoting myself: "This is a social problem, influenced by language design."
>This isn't Java's "culture" to not handle errors at all. Go's conventions imposed in its std libs are certainly not a superior model by any measure as I previously showed.
This is java's culture, and I assert it from experience. It is a result of the language design (and communiques from language authorities).
You only showed that it was possible to misbehave in Go, which is totally uninteresting. You have failed to show that your theoretical problem is a practical problem. This is a perfectionist argument.
Having worked on golang code bases for several years now, I assure you it's not a theoretical problem. It's not fun to have ignored or overwritten errors that keep the program going as if nothing is happening.
If you overwrite or ignore your variables you're going to have a bad time. Period. That's not unique to errors. I'm not sure you've said anything to convince us that making mistakes around errors alone is anything more than theoretical.
Beyond defining an interface named error (early versions didn't even offer that), Go does not have a concept of errors, so I'm not sure it is even possible for there to be a problem specific to errors. Certainly Go could do more to help you with correctness in general.
Because errors are just return values in golang, and it's not possible to write a linter to guarantee that they're handled, there's always going to be the possibility of them being overwritten by accident. Not to mention due to the lack of composability, you end up with even more hacks like what gorm does, making it even easier to mishandle errors.
> Go does not have a concept of errors
Exactly the problem. There needs to be a notion of error handling in the language.
> Because errors are just return values in golang, and it's not possible to write a linter to guarantee that they're handled
That's true of all types, though. Nothing specific to errors applies there. Spend enough time in Go and that's going to bite you, even when the error interface is nowhere to be found. You've still not made clear what's special about errors that makes the concern about errors, not all types, more than theoretical.
> There needs to be a notion of error handling in the language.
Why? The computer doesn't have a concept of errors either. All the computer offers is different states. What Go could offer is more guarantees around ensuring that you've handled different states correctly, but that has absolutely nothing to do with errors specifically.
Now, humans have a concept of error. Of course. We classify specific computer states as being errors. But since the language and computer treat errors and other states as being the exact same thing, why would that human classification of error magically lead you to start making mistakes that you wouldn't make with other states? That doesn't make any sense.
Here's a thought: Stop classifying those states as errors. Call them "happy fun times". Then you won't have whatever mental hangups around errors that are causing you problems specific to errors. Because, from a technical perspective, errors don't exist. They are entirely a human construct. There is nothing about that human construct that should be tripping you up in the tech.
No, there should not be any special concept for errors. Errors are not special. As far as the tech is concerned, errors don't even exist. What Go could do is improve on state management generally. Applicable to not only states you've decided to call errors, but all other states as well. Anything that tries to improve state management for only what you've decided to call errors still leaves all of the exact same problems dangling for every other type. That is poor design and doesn't actually solve the problem.
> You've still not made clear what's special about errors that makes the concern about errors, not all types, more than theoretical.
The way errors are handled in golang, its possible to ignore them accidentally. This doesn't happen with other types because you don't keep constantly overwriting the same variable over and over (immutability is generally good, another thing that golang lacks), increasing the likelihood of ignoring an error.
E.g.
a, err := foo()
if err != nil { ... }
b, err := bar(a)
c, err := baz(b)
A linter will complain if b or c are unnused, but it cannot complain that err is unnused, because it is used and has been declared before. Whats worse something like this
a, err := foo()
if err != nil { return err }
b, errBar := bar(a)
if errBar != nil { return err } // oops
> The computer doesn't have a concept of errors either.
This is exactly the same reasoning that golang authors gave about why it doesn't have a notion of optional or non nullable pointers. To the computer, a pointer is a pointer. This isn't the way to think if we want to make reliable and readable programs. Computers don't have notions of methods or inheritance or even functions either, everything is a jmp of some sort.
The entire field of programming is to make it easier for humans to reason about code, OOP, functional programming, etc. Otherwise, we'd all be writing assembly or machine code.
Errors are special because they need to carry certain state about the program when an error happened (e.g. stack trace). golang errors are basically strings, which is why people invented frameworks to capture stack traces in golang errors. On the code bases I worked on, this building of the call stack is either done manually (by wrapping errors) or by concatenating strings, or by logging errors everywhere and capturing the stack at the log site. Quite horrible experience overall and just keeps polluting the code with boilerplate the makes it even less clear what's going on.
I've experienced the issues that come out of golang's overly simplistic design. The overly verbose code that is hard to see what its doing at first glance, the non-composability of errors, the mishandling of errors, etc.
Other than exceptions, languages like Rust have it much better than golang. You still get to have explicit error handling, but with much superior ergonomics that make them much more difficult to mishandle.
> you don't keep constantly overwriting the same variable over and over
Except when you do...
> Whats worse something like this
Yes, this could be catastrophic.
dir, file := path.Split(fileToKeep)
dir2, file2 := path.Split(fileToDelete)
if file2 == "x" {
removeFile(file)
}
What does it have to do with errors?
> golang errors are basically strings
That's not true at all. The error interface asks that you provide a string representation of your error when called upon (i.e. the Error() method), but you don't want it to be a string underneath. Further, you don't even need to use the error interface. Not all Go functions, even in the standard library, return errors declared using the error interface.
> Errors are special because they need to carry certain state about the program when an error happened (e.g. stack trace).
A stack trace is useful when you have an exceptional circumstance, but Go's exceptions (panic/recover) already provides that for exceptions. We are, however, talking about errors, not exceptions. Why would you ever need a stack trace when the network is down, for example? There is nothing unusual about the network being down. It's just another happy state along your happy path and logically dealing with it is no different than branching on the user inputting 1 vs. 2. Should languages also have special constructs for dealing with that? Of course not.
> I've experienced the issues that come out of golang's overly simplistic design.
No doubt. I have made clear that Go could improve here. None of those improvements are specific to errors, though. The idea that errors are special just doesn't jive. Every problem you encounter with errors will also be encountered with other types sooner or later.
> languages like Rust have it much better than golang.
Rust is a good example of how errors aren't special, though. The constructs you may use for errors in Rust are built upon lower level features that help with state management generally. Mentioning Rust here is a is a bit strange seeing as how it contradicts the entire premise you're trying to push. The features of Rust that can help you with errors can also help you with problems of other types. Rust is a good example of how good design can work to solve the problem generally, not just for errors like you want to do.
To really get to the meat of this, your entire comment sums up to say that Go could do more to help you with state management. We already established exactly that in previous comments. Where do you think errors specifically begin to come into play?
The fact that the former scenario is much more likelier to happen in golang due to its error handling. The former can be caught using unit tests, and should stick out more in languages like Java with scoped try-with-resources blocks that limit the scope of lifetimed variables (another bad design in golang where defers are function scope, not local scope limited, introducing unnecessary runtime costs, while also being less useful in practice).
At the same time, you don't want to be writing unit tests for every simple scenario. Some people who use dynamic languages argue that you just type assert everything in unit tests and you should be good. Obviously, that's not how things work out in practice. Similarly here, because of the way errors are handled in golang, the same pattern is everywhere in the program, and it is extremely tedious to write the same unit tests over and over everywhere, while making sure to reach a certain level of coverage.
> Not all Go functions, even in the standard library, return errors declared using the error interface.
Do they just return integers then like C?
> Why would you ever need a stack trace when the network is down, for example?
Because it's still important to know where you are in the program when the network broke, in order to make sure that recovery happened correctly for example. Seeing a "network is down" error in the logs is useful, but more so is knowing exactly what I was doing (what if I wasn't expecting to access the network in a particular path? etc.)
> Every problem you encounter with errors will also be encountered with other types sooner or later.
Practicality matters, otherwise C is all we'd ever need, and we wouldn't be having constant CVE's etc.
> Mentioning Rust here is a is a bit strange seeing as how it contradicts the entire premise you're trying to push.
Not really. Rust errors compose nicely, and you're explicitly forced to handle them (unlike golang's), and are much harder to accidentally swallow or ignore compared to golang. This scenario is repeated many times when you compare golang to other better designed languages. The language constantly takes the easy way out so to speak, making the compiler implementation simpler, while pushing complexity onto the user. Furthermore, as I pointed out, there's nothing preventing us from using the same approach in Java, now that it has sealed types and pattern matching.
That is not possible in golang due to its lack of support of all those constructs. Now with generics, some people are starting to use some similar approaches, but they fall flat on their face because (1) golang doesn't have those constructs as I mentioned, but also (2) the way generics have been implemented in golang (similar to the rest of the language) are simplistic (you can't have types on member functions), leading to further abominations and verbose code.
Java's checked exceptions are not much different actually. They still require you to handle the error either by try/catch or by declaring that the method throws the same exception, or a superclass of it.
I also remembered another shortcoming of golang error handling that I've seen several times in real code bases, being forced to check both the error and the other return value to make sure that things are working. Yes, a properly written program shouldn't need to do that, but reality doesn't care. And what's ironic is that golang was supposed to be designed to support "programming in the large" (another unverified claim that is contradicted by reality). The fact that it opens these doors is indicative of the mentality that went into designing it.
> At the same time, you don't want to be writing unit tests for every simple scenario.
Yup, exactly. In a perfect world you would, but the world ain't perfect. Sooner or later you're accidentally deleting the wrong thing because you didn't test it/didn't test it correctly.
And for what reason? A language can guard against that kind of mistake quite well.
So, that still leaves us wondering why you don't find it advantageous for a programming language to be able to deal with these kinds of problems unless the problem is related to an error? If you have a solid foundation the problem with errors you are trying to show would disappear. Go only has problems with errors because it also has problems generally.
> Practicality matters, otherwise C is all we'd ever need, and we wouldn't be having constant CVE's etc.
Agreed. Which leaves it to be insane to only want to fix the problems with errors when everything wrong with errors is also wrong with every type. If you fix the error problem properly you've automatically fixed it for all types. Why on earth are you suggesting that you'd purposefully make the fix harder just to ensure that it only fixes errors?
> Rust errors compose nicely, and you're explicitly forced to handle them (unlike golang's), and are much harder to accidentally swallow or ignore compared to golang.
This isn't a feature of specialized error handling, but Rust's overall design towards helping with general state management. Rust alleviates the same problems for all types. Rust yet again shows us that errors aren't special and don't matter. Provide a good foundation for state management and managing error state becomes easy by virtue of just being yet another state. You are able to cleanly deal with errors in Rust because it took care to get the problem right generally, not because it took care to worry about errors at the expense of everything else.
Go could, like Rust, do more to help with general state management. It would be insanity to try and only do that for errors, though.
> You have a discipline issue with Java, it is not an issue in the language itself, it's with you and your team.
Only if you consider the "language" to be only the language itself, i.e. syntax + semantics. When people refer to a "programming language", they almost always mean the whole package, i.e. the language itself, the community, package ecosystem, tooling, conventions, etc.
If there is widespread usage of an incorrect pattern, then it is the failure of the language for not addressing it. For example, C's lack of package/build management.
The experiences with exception-based languages others in this thread brought up mirror my own. No thinking about error handling whatsoever, empty `catch` blocks, dumping stacktraces in lieu of error messages, etc.
Needing discipline to prevent those is exactly the problem. The language is what should guide you towards a better way, in this case explicitly returning errors.
> Only if you consider the "language" to be only the language itself, i.e. syntax + semantics. When people refer to a "programming language", they almost always mean the whole package, i.e. the language itself, the community, package ecosystem, tooling, conventions, etc.
You're making a lot of assumptions based on absolutely no factual data, only your alleged experience. I can dismiss all your arguments easily as anecdotal.
As I said earlier Go suffers from having a bad exception system itself (panics/recover) and on the top of that go makes it a breeze to ignore errors:
_ := YieldError()
You're basically claiming that teams are magically more disciplined just because they use Go. That's of course complete bullshit. An undisciplined team will ignore errors and exceptions the same way. There is nothing in Go that compels anybody to handle errors, just like there is nothing in Java that compels anybody to ignore exceptions.
Go errors are purely a convention, Go doesn't have anything in its syntax that forces error as value, it's just what the std library uses. So when talking about Go errors, they are purely talking about a convention, with absolutely no syntax to compel people to follow or enforce that convention. It requires thus even more discipline. Do you really believe your undisciplined programmers that ignored Java exceptions because it's verbose to deal with them are going to magically adopt an even more verbose paradigm? Of course not.