6 ms·
Go's error handling is a horrible mess: 1. It's easy to ignore returned errors without any compiler warnings. You have to rely on third party tools such as gol
by janosd 3y ago
Go's error handling is a horrible mess:
1. It's easy to ignore returned errors without any compiler warnings. You have to rely on third party tools such as golangci-lint to report missing error handling.
2. Errors don't carry stack traces with them, you have to rely on third party libraries or custom errors to get that functionality and you will only get it for your own code, not in other libraries you are using.
3. It's unclear who should add context to error messages is it the caller or callee? Usually it gets skipped, leading to useless error messages.
4. Errors are untyped. If you want to decide based on error types, you have to use errors.Is or errors.As, which, surprise, is roughly as expensive computationally as panic-recover. (Source: I did a performance tests on this with Go 1.18) Go might as well add a simpler way to create exceptions. (I wrote a prototype library to that effect a while ago: https://github.com/APItalist/lang https://github.com/APItalist/lang )
5. Error messages are too terse and hard to read when using the recommended semantic of "message (cause(cause(cause)))". I'd rather see stack traces, that's much more useful.
6. Most loggers are globally scoped and cannot be injected into code, leading to an all-or-nothing approach. It is not uncommon that you have 3-4 logging libraries as dependencies, which you need to configure separately (if you even can). Also, good luck securing this mess.
- notTooFarGone 3y agoCalling a linter thirdparty in Go is really disingenuous. Like you install go in your favourite IDE and it's batteries included. It's part of the standard set.
- aniforprez 3y agogolangci-lint does not come batteries included. It is a third party library. Saying it's "part of the standard set" is really disingenuous
- ryapric 3y agoI wonder if they might be talking about `go vet`?
- fulafel 3y agoSeeing the "golang" spelling as part of the project name is a sure sign something is not first party.
- janosd 3y agoGo is a weird mix. It doesn't even let you create an unused variables, but happily lets you ignore errors or return variables. That makes no sense and is on Go, not on the admittedly quite excellent tooling provided by people who are not the Go dev team (third party). An IDE is just as much third party to the language as golangci-lint is.
- aatd86 3y agoIgnoring an error is a red herring. You have to go out of your way to actually use a special character to do it. No, one real issue that can happen if one is not careful (but fortunately linters help) is variable shadowing which may lead to some errors being unchecked. In general, I find that error handling is not as horrible as some seem to purport.
- janosdebugs 3y ago> You have to go out of your way to actually use a special character to do it. Only if the function returns more than the error. You can happily do this without errors: fh = os.Create("/some/file") defer fh.Close() Needless to say, this is a terrible idea if the underlying filesystem can give you an error at close time, e.g. on NFS. The correct way to write the above code would be: fh = os.Create("/some/file") defer func() { if err := fh.Close(); err != nil { // Do something with the error } }() Yet, I see a lot of the former and very few instances of the latter.
- aatd86 3y agoOh you're right. I had forgotten about that. I think it's mostly an API legacy mistake. Close should probably return (bool, error). Probably a remnant of coding in C wrt sentinel values.
- janosdebugs 3y agoYou could still ignore both returned values. Go shouldn't allow ignoring returns without explicit dogsleds (underscore) at all if it were to stay "in character".
- konart 3y ago>3. It's unclear who should add context to error messages is it the caller or callee? Usually it gets skipped, leading to useless error messages Why is that unclear? Let's say you are writting a db client package and a service around it. The package's db.Exec(query) method should return and error that will have an error text received from db if any AND\OR context from the package itself. Then in your service you add additional context to this error if needed. Finally you log your typical "failed to write HackerNews comment do db with err: %db_package_context: db_error_text_here%" >6 Not sure about "most" loggers, but I have no problem with zap. Popular, definetelly can be injected etc.
- TheDong 3y ago>>3 > Why is that unclear? The usual advice is to follow what the stdlib does. Let's look at an example. Let's say we close a file and then try to set a deadline on it: f, _ := os.Create("/tmp/filename") f.Close() fmt.Printf("%v", f.SetDeadline(time.Now())) // output: use of closed file Okay, so in this case, it's the caller's responsibility to keep track of the filename and add the context of what file was already closed, resulting in that error. However, what about the error for trying to write to a closed file? _, err := f.Write(nil) fmt.Printf("%v", err) // output: write /tmp/filename: file already closed Oh, I see, it's Write's responsibility to add the context of the filename. Huh. This is a clear example of the problem the parent is talking about. The 'os.File' construct knows the filename. Sometimes it adds that as context to errors, sometimes it doesn't. Sometimes the caller needs to add it in, sometimes the callee has already added it.
- masklinn 3y ago> The usual advice is to follow what the stdlib does. This seems to be a significant problem in general, because gophers want clear direction (after all the language was created specifically for… choices to be limited) so they take quips as gospels, but robpike, rsc, etc… take them more as suggestions / guidance (90/10, possibly even 80/20) to be moderated by taste. I don’t remember which one but I think it was robpike who expressed frustration on one of the recently popular issues / proposals, because the proposal was essentially to legislate one of the more common quips, and they like being able to break those when useful or convenient. I think there was also something similar to your exploration here with zero values, where despite the quip that you should “make the zero value meaningful” multiple standard library modules will straight up panic if fed zero values (a classic example being the File struct, `File.Name()` panics and pretty much every other method returns ErrInvalid, so a zero-valued File is useless, actively problematic, and the source of unnecessary overheads). An other fun one is that you can’t call IsZero on a zero `reflect.Value`, and the error message is quite amazing: panic: call of reflect.Value.IsZero on zero Value You need to carefully read the doc and notice that th middle paragraph documenting Value itself says: > The zero Value represents no value. Its IsValid method returns false, its Kind method returns invalid, its String method returns “<invalid Value>”, and all other methods panic.
- nbraxf100 3y agoThe error handling is second nature to anyone who has done C or Unix programming. It just feels dirty not to check for an an error. This is one part I like about Go.
- nprateem 3y agoWhich rules out the majority of people who learnt to code in the last 25 years (many unis have taught java since 2000ish)
- rob74 3y agoBecause people don't ever learn more about programming than what they were taught in university? If this is true, I'm a bit afraid about the career perspectives of these students, and wouldn't really want to be on a team with them...
- nprateem 3y agoOnly a minority will be sufficiently sadistic to learn C when better alternatives exist. So yes, the above excludes the majority.
- kaba0 3y agoAn if err with some random one-liner in the err part is not error handling. You can’t reasonably handle an error condition on a local basis, that’s why exceptions (especially checked ones) are superior. They do the correct thing — either bubble up if it doesn’t make sense to handle them in place, or have them in as broad of a scope as it makes sense with try-catches. Oh and they store the stacktrace, so when an exception does inevitably happen in your software you will actually have a decent shot of fixing it instead of grepping for that generic error message throughout the program (especially if it’s not even written by you!). I swear people lie to themselves with all those if-errs believing they have properly handled an error condition because it took effort.
- arez 3y ago
- amedvednikov 3y agoOne of the main reasons I created V. It's pretty much Go with Option/Result that forces you to handle errors: f := os.create('foo.txt') or { println(err) return } https://vlang.io/compare#go https://vlang.io/compare#go
- appleflaxen 3y agoIt sounds like you think about error handling a lot. Is there a language that has error handling "done well " that you like?
- janosdebugs 3y agoError handling is a difficult topic. Generally, the more you can catch in the compiler, the less you have to write runtime checks and the obligatory unit tests that everyone likes to forget. So if you are on the lookout for a language, I'd look for something that has explicit nullable/non-nullable types, as well as strict and static typing. However, I wouldn't pick a language purely based on its error handling capabilities. That's treating everything like a nail just because you have a hammer. I'd pick a language that's suitable for the task at hand. Go is suitable for making small(ish) webservices. Over 10k lines of code it becomes really hard to keep things straight. However, that's more due to its very limited scoping abilities. As far as Go is concerned, you can make the error handling work. In ContainerSSH, we built our own logging overlay, which you can find here: https://github.com/ContainerSSH/libcontainerssh/tree/main/log https://github.com/ContainerSSH/libcontainerssh/tree/main/lo... This companion message library has a custom error structure that carries along an error code, which uniquely allows identifying the cause of the error: https://github.com/ContainerSSH/libcontainerssh/blob/main/message/message.go https://github.com/ContainerSSH/libcontainerssh/blob/main/me... Errors can be wrapped and we added tools to determine, if a certain error has an ancestor with a specific code, allowing for tailored error handling cases. We also added a tool that gathers the comments from the error code constants and adds them to the documentation: https://github.com/ContainerSSH/libcontainerssh/blob/main/cmd/generate-message-codes/main.go https://github.com/ContainerSSH/libcontainerssh/blob/main/cm... I hope this helps.
- maleldil 3y ago> Over 10k lines of code it becomes really hard to keep things straight. However, that's more due to its very limited scoping abilities. Could you please elaborate more on this?
- 3y ago
- throwaway894345 3y agoI agree that there is room for improvement, but I don’t mind Go’s errors that much. Using a linter to make sure errors are checked doesn’t seem like a major problem (you have to run a linter anyway, so what’s the harm?); most Go developers reflexively check errors for everything besides fmt.Println anyway. It would be better to put this in the compiler I suppose, but not a major deal. Also worth noting that Rust doesn’t require you to use errors either; unused errors are a warning in the compiler unless the return type is a result AND you’re trying to access the valid data. This is a better than Go, but not by much in practice. The error interface doesn’t bother me too much either. Just use errors.Is/As to determine the type of you’re going to do something special with it. It’s way better than having to create unique error/result types for every function. Who should add context is definitely a problem, but I’ve settled on “the callee”, but in cases where you’re calling something that doesn’t add context you will need to add it in the immediate caller. Adding context in Go is significantly easier than in other languages (yes, you can use anyhow in Rust, but it’s not considered good practice to put this in library code), and good context largely obviates the need for stack traces anyway. Context is nicer because it can tell you, for example, which loop iteration you were in when things blew up or what the salient parameter values were—stuff you don’t get from a stack trace. Of course, you have to do a bit of work for this benefit, but fmt.Errorf makes this super easy. Logging also irks me. You can pass a logger like any other data, but mostly people just use global loggers. I haven’t had the multiple loggers problem, but that’s because library authors in Go idiomatically do not add their own logging. What are the languages that do logging well? I’ve had a horrible time with Python (and I think Java but it’s been 10 years).
- simiones 3y agoUnfortunately, fmt.Errorf makes errors.Is/As useless. In fact, errors.Is is mostly useless in general, since very few Go libraries have any error types at all. You're usually stuck with parsing error messages if you actually want to handle errors programmatically, even for much of the standard library.
- philosopher1234 3y agoUh? If you use `%w` in fmt.Errorf(), it should still work with .Is and .As?