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. This is not possible in golang.
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.
> I don’t think I’ve ever seen a case where a Go function returned neither a value nor an error, or both a value and an error.
That's kind of the point. The type system should be powerful enough to disallow those cases then.
In practice, I've seen both, always accidentally. I've also (more commonly) seen a lot of confusion and annoyance around:
Okay, so this has to return a pointer for the error case, should the caller check that? If not, how do we square that with checking for nil pointers being generally a pretty good rule? If we do check, our unit test coverage has a blemish for every call since nothing can hit that. If we skip it being a pointer, then it's a zombie object.
It's just a lot of cognitive load and bikeshedding around an issue that shouldn't exist.
> I’m not a go developer. How does go document how a function can fail?
There's no magic to it. Errors are values, so it's a part of the function signature that there's an error code to check. In C++ any function can throw an exception and there's no way of knowing that it wont.
It's true that go doesn't document what _kinds_ of errors it can throw, but at least I know there's something to check.
> If something blows up in production in an unexpected way, sometimes that's okay. You log the problem, fix it, and it's fine.
as the person on call for such events, it's really not fine.
would you rather handle these errors upfront during development time or unexpectedly and uncontrollably, during runtime? having been on-call in one way or another for ~10 years, i know which i'd prefer.
after having used golang in production, i will never go back to using a dynamically typed language if i can avoid it. almost 0 thought or effort is required to handle errors correctly in go, and it's still possible to ignore them (just use `_`). not that i'd recommend ever doing that.
> Literally the first non-trivial code I wrote in go… I knew exactly where and how things could fail and where things were failing just by looking at the code.
Could you give an example?
I think you’re talking about something different than what I’m understanding. One of the major frustrations I have with Go error handling is the lack of stack traces, which means I often have to modify code in order to find out where an error occurred.
I’m pretty sure that’s not what you’re talking about, though.
> why the languages newer than Go, including Rust, also keep a clear division between errors and exceptions.
There are also languages older than Go that make this distinction. Java for example. Like Rust (and in contrast to Go) they even have syntax for convenience, and the compiler checks that you handle them.
You know how you can immediately spot old Java APIs and code? The one that was designed and written while we lived through the suffering? Whenever you encounter checked exceptions. Turns out there is no (or even negative) value in this rather arbitrary separation.
Special how? Surely they are not special in a way that can't be captured by a type system. You say they should be handled in a special/deliberate way, but that's what it means for something to have a distinct type: it can't be handled like everything else. You just define special error-handling functions and they will work on errors and nothing else, and nothing else will work on errors.
As far as syntax goes, the I feel the big issue is about deferring error handling. You don't always want to mix error handling code with your algorithm logic, so you need to defer. We also talk about 'errors' so generally that we never bother to qualify which kinds of errors should be deferred or not in a way that is encapsulated in the type system. Or what kind of deferring should be available -- return errors or just move them later in the same scope? (Java's checked exceptions are a good example of attempting to do this, but it doesn't work out in practice. Programmers don't think like programs do: deferred error handling isn't the same as deferred error-handler writing, and both are needed in different ways.)
From what I can tell about error handling in Go, it isn't really a step forward. (And we really want a step forward.) Go just kind of falls back on "this was the last thing that worked, and making significant progress is probably too hard, so we won't bother trying."
That's the impression I get, at least. I don't actually do any Go programming, but I'm not inspired by its approach, either.
True but you could forget to do that in your alternative example as well (albeit you'd get no instances of a assigned rather than sporadic instances - however the point of testing frameworks is they should catch that kind of human error regardless of the language or it's syntax)
> Or even assigning it to a different variable `b`.
Actually the compiler would catch that too.
> They add up quite quickly in any sizeable project. It's not just one offs here and there, you end up with functions littered in the code base that distract from quickly getting to understand the code when debugging a bug or an outage.
I guess it depends on what you're used to but I've always preferred code that abstracted away longer or more complex functions into smaller ones. I guess that's the FP habits in me transferring over to imperative languages. In any case, I've never had any issue debugging such code when under pressure during an outage.
> The thing is, golang seems to be used just because it has Google as a name behind it,
That's complete and utter BS. In fact I had already cited reasons I like Go. so please don't assume that because you don't like something then it's automatically a bad language that other people can't like either.
> This shows up time and again, as people are not able to defend the technical choices behind it in any proper way,
People have and they do though. The issue is that there is a hell of a lot of language elitism in the the different programming language cults so it doesn't matter what reasons we might give - you've already decided you don't like feature x and thus our reasons are wrong by default. You demonstrated just this kind of mentality when you argued that people use Go just because of Google in a reply to a post that lists why someone likes Go.
> which has already been shown several times that the golang authors are quite bad at defending their decisions, and they're not experienced at language design to begin with
The arrogance in that comment is overwhelming. I look forward to the day you create something worthy of recognition and the inevitable backlash on HN from kids who assume they're smarter then you are in every way. This isn't me putting Russ Cox et al on a pedestal - this is me pointing out that talk is cheap and most of the critics on HN are just average engineers at best.
> Golang would probably be fine for some small tools here and there (even for that, Rust or AOT Java would be a consideration), for anything larger, a static JVM or .NET language is strictly superior.
Clearly not everyone agrees with you there because a lot of successful important projects are now based on Go.
You need to learn that features you prioritise are not going to be the same as features others might prioritise - and that having different priorities doesn't make one party wrong and another right.
this is the sort of silliness that is common in the Go world. as noted elsewhere, errors are values in lots of languages - Rust, C, Scala, Haskell, etc etc, but Go explicitly has no way to handle them nicely, no specific syntax and no fancy type system stuff like sum types.
it is my very strong belief that this will eventually be fixed in Go and when it does, almost all the people currently saying "I like errors being values [and it's fine that Go makes it very annoying]" will quickly prefer having some actual language help for these values.
> More generally though, I wish we could avoid having exceptions to begin with. What's the reason behind their prevalence in almost every language?
Because nobody really wants low-level stuff like divide by zero, memory errors, index out of bounds wrapped as a `Result type ? Both Rust and Go have panics. Every function would effectively return a `Result` type since any non-constant expression can hypothetically return an error.
> I strongly believe in halting on error as the default response, and unwinding until the correct layer can proceed.
Isn't this is exactly what happens in Go code with the current implementation? At least all the Go code I've read seems to do this.
If you don't know what to do or can't do anything, you return the same error, preferably adding more details to the error string. If you do know what to do you handle it.
I much prefer that everyone has to make an explicit choice on what to do; I find following code with exceptions much more painful.
> This really irritated me when I started working with go, but it stopped bothering me and now I even mostly like it.
Don’t get me wrong I like a good unused code warning.
What frustrates me is that Go’s is dumb / unreliable, and it will stop you from working entirely until you’ve complied with this whim, which has a fraction of a percent chance of identifying a real bug.
> Basically writing go without `staticcheck`[1] is not recommended.
So why have these things as mandatory compiler errors?
> What's the reason behind their prevalence in almost every language?
More often than not it's not the caller that is responsible for handling errors/exceptions.
When you force the caller to take care of every single error, you end up with unreadable boilerplate code which hides the actual logic. There's a reason why Rust ended up with the `?` syntax sugar.
On top of that exceptions will occur. You can't pretend they won't and kill the app if they do. Again, even Rust and Go ended up adding handlers for their brain-dead panics.
Exceptions (when wielded correctly) end up simplifying your program. You develop for the happy path (mostly), and let code at the higher level of hierarchy make decisions about unhappy paths. That's how you get Erlang's supervision trees (https://erlang.org/documentation/doc-4.9.1/doc/design_princi...)
> I think just forwarding all low-level errors is a really bad habit
Why exactly is that a bad habit? In almost all situations where I return an error I already have enough context, I'm just wondering what else I'd add to that.
> And unlike Go or C, I don't have to remember to check the error every single time.
How do you mean? I’ve been writing Rust frequently lately and you still have to check the case. Do you mean that the compiler will nag you if you stunt so you don’t have to remember to do it because you’ll be reminded?
That’s one of the things common Go linters do that I wish was built into the compiler. It’s silly that an unused dependency is a compiler error but an unchecked error/return value is not.
> I really dislike exceptions because there's no documentation for how a function can fail. For this reason I prefer go style errors, which are an improvement on the C error story. Yes it has warts, but it's 80% good enough.
I’m not a go developer. How does go document how a function can fail?
A Java developer can use checked exceptions so that some information is in the signature. For unchecked exceptions the documentation must explain.
I guess in Go the type of the error return value provides some information but the rest needs to be filled in by the documentation, just like the Java checked exceptions case.
> so I avoid finding out about an unexpected edge case at runtime in the first place!
This has not been my experience with Rust. In my experience I'll handle all the errors just fine—nothing is unexpectedly crashing—but then every once in a while an error object will propagate up and I'll have no clue why that error got triggered by that input. If I'm lucky I'm working on something that makes dropping into the debugger easy, but other times this is the beginning of a very long troubleshooting session that would have been much shorter if I had a stack trace.
Mind you, this always does turn out to be a mistake in my code that caused an error to get returned incorrectly, but Rust doesn't help me find and fix these problems the way that Java does.
(My personal opinion is that Java's checked exceptions were on the right track and just need some UX improvements and syntactic sugar for handling them. They provide all the safety of a result type with all the debugability of an exception.)
> 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.
> I have seen experienced programmers doing this pattern again and again and they are really convinced that it is helpful and consistent.
In a nutshell, this is why I hate working in go. While I'm sure your code, dear hn golang fan, doesn't suffer from this, much of the go code I've had to deal with extends the principle of “unaware of many common failure modes and swallow them into the exit code” to every function.
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. This is not possible in golang.
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.
reply