3 ms·
> 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 handle
by azth 5y ago
> 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.
- randomdata 5y ago> 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?
- azth 5y ago> What does it have to do with errors? 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.