20 ms·
Exploring Error Handling Patterns in Go
- tln 8y agoI appreciated the article, but the error handling in Go just bugs me. So verbose... The built-in tool does not even warn about unused errors... not `go build`, and not `go vet`. What's more important, an ignored error or an unused import? https://play.golang.org/p/j-oXsZz51ki https://play.golang.org/p/j-oXsZz51ki
- pstuart 8y agoThat's by design -- programmer's choice.
- stouset 8y agoAnd it’s—bluntly—a terrible design.
- dfischer 8y agoWhy? I am genuinely curious. Terrible compared to what?
- lozenge 8y agoI used to forget to put "set -e" in bash scripts. Then one went off and deleted a whole bunch of important stuff despite a prerequisite command erroring. Now I include it, but remembering one line of code in the header is easy compared to Go's approach of remembering to check every error.
- latch 8y agoExceptions. Anders give an interview in 2003 [1] where he talks about how C# looked to learn from Java's checked exceptions. His conclusion was basically that, in their evaluation, 9/10 exceptions cannot be handled beyond some generic top-level handler. If this observation is correct, and it certainly aligns perfectly with my own, then bubbling makes a lot more sense. Note that, with error return values, you can emulate bubbling (which is what most Go programmers end up doing). And with exceptions, you can emulate return values. The question is what's the most common default? And, again, according to Anders, as well as any project I've ever worked on, bubble-by-default is overwhelmingly the most useful thing to support cleanly. The only way Go's approach makes sense is if you consider it's original goal (system programming) and MAYBE (i don't know, I'm not a system programmer) for such systems you can/need to handle each error. Except that's not really how Go is being used now, so... [1] https://www.artima.com/intv/handcuffs.html https://www.artima.com/intv/handcuffs.html
- jy3 8y agoI've always been annoyed by the parallel control flow introduced by exceptions in any language. They are used so often in many languages where it doesn't feel necessary. The fact that I don't even have to think if the function call I'm looking at can throw and if I should catch it or not outweighs everything.
- groestl 8y agoEasy, just assume it throws. That's the case anyway. Thanks to panics, even in Go. Edit: Also, there is no parallel control flow. Languages with exceptions have union-type return values, and every statement is implicitly followed by the equivalent of: if err!=nil return nil, err. The fact that in Go you have to type that makes Go cumbersome, not smart.
- cube2222 8y agoNot really, in all the years I've been writing Go, only one library used panics for error handling. Usually if something panics you don't want to handle it. (Other than at the http handler level, where you can just throw an InternalServerError and log the panic) First and foremost, you can usually assume libraries won't panic, though it would be nice to have a tool (grep) to check for explicit panics.
- groestl 8y agoIf something "errors", you usually don't want to handle it either. Other that at the http handler level.
- cube2222 8y agoActually, you often do. That's the point really. You should decide if it's an operation you might want to retry, you might also want to just flat out error and do nothing more, maybe you want to provide degraded functionality, like provide some default answer. I think errors as values cause you to always think about this, which makes you handle errors in a more sensible way, instead of just bubbling up. Sure, 90% of situations you will bubble up, but in my opinion it's still worth it.
- cube2222 8y agoI actually really like Go error handling (I write Go daily), but truth be said, they're a poor mans Either Monad.
- pmarreck 8y agoThe thing that bugs me is that if you ignore the error (by simply not checking for it), it's still there, possibly insidiously corrupting runtime state. Imagine trying to debug a file format corruption that happened because some obscure part of the code tried to add to the format and instead errored (silently) and added garbage and then the code just kept chugging along until the state REALLY messed things up. The thing many programmers don't seem to realize is that a program is a model of a design in the programmer's mind. If the model goes off the rails of the expected design/behavior in any way, that should be considered very bad ASAP... or as many languages treat it, "exceptional".
- TheDong 8y agoI don't understand. Go errors out on unused imports, but you can type "import _ foo.com/unused-import" to not error out. Why doesn't 'errors.New("asdf")' error out and require you to instead write '_ = errors.New("asdf")' to ignore the result I think the real answer is not that it's intentional design, but rather that the original compiler was not powerful enough to implement that feature easily... and once go hit 1., it was impossible for them to add new warnings or errors because there are no warnings and errors are backwards incompatible. Sure, that means developers use third-party tools for warnings because the go compiler refuses to ever have warnings (that compromises the pure beauty of the language obviously), but at least that means it's only the users that have to deal with the complexity of using more tools, the compiler developers can ignore it.
- mopsy 8y agoI don't think it's because of the complexity. I'm quite sure it would not have been difficult to do. One of the reason is that you don't always want to check the error. The most common one is fmt.Println. I would not like to always write _, _ = fmt.Println("Hello, playground")
- stubish 8y agoYou certainly do not always want to write _,_ = fmt.Println("Hello, playground"), and I think this points out the real lack. What do you want your program to do if fmt.Println starts failing? I think, unless you are explicitly checking for errors, that in all other cases you want it to crash. Rather than silently continue. Which is a bug, and a potentially disastrous one, that is endemic in Go code (and, to be fair, plenty of other languages). Thankfully the practical risk of this particular case is tiny. This is why you want unchecked errors to implicitly bubble up. Which is what exceptions give you, or perhaps a syntax with implicit error return values rather than Go's by-convention approach.
- pstuart 8y agoIf fmt.Println fails you've likely got bigger problems and the game is over. And if it means a lot in this case, wrap the function and use it instead.
- rco8786 8y agoif err != nil return err if err != nil return err if err != nil return err https://github.com/docker/cli/search?q=%22if+err+%21%3D+nil%22&unscoped_q=%22if+err+%21%3D+nil%22 https://github.com/docker/cli/search?q=%22if+err+%21%3D+nil%... https://github.com/kubernetes/kubernetes/search?q=%22if+err+%21%3D+nil%22&unscoped_q=%22if+err+%21%3D+nil%22 https://github.com/kubernetes/kubernetes/search?q=%22if+err+... https://github.com/coreos/etcd/search?q=%22return+err%22&unscoped_q=%22return+err%22 https://github.com/coreos/etcd/search?q=%22return+err%22&uns... https://github.com/influxdata/influxdb/search?q=%22if+err+%21%3D+nil%22&unscoped_q=%22if+err+%21%3D+nil%22 https://github.com/influxdata/influxdb/search?q=%22if+err+%2... The reality of Go's error handling is that you just implement exactly what exception bubbling does painfully by hand.
- chmike 8y agoThis is not correct. Exceptions do different things than report an error. They unwind the stack. That's why they are called exceptions and not errors. One important benefit of Go's error handling pattern is readability. With exceptions, it's not easy to see who handles it and where. There is indeed less code, and that's nice for the writer, but from the reader perspective, error handling becomes obscure. And from the quality control point if view, this becomes unsafe.
- deleted 8y ago[deleted]
- h1d 8y agoAt the cost of making the entire logic's readability less which to me is more important than sometimes getting confused where errors bubble up to. The philosophy is different when, for example the author of Ruby wanted to make coding fun for programmers and does a good job at it and Go is sticking to 'this must be right' approach and breaks some people's heart. Personally I'd appreciate being more 'fun'.
- dfischer 8y agoI used ruby for a long time and Go more recently. I think Go is fun. I’m able to read code bases with consistency. In a lot of Ruby apps, I see creative flexing that is unique to that person, or teams style. The fun part is getting code written, and shipped. And it stays fun when it’s maintainable and production ready. I’m having a lot of fun shipping Go code. :) I definitely can understand a codebase a lot faster than a random ruby one. That may be a personal thing but it works for me.
- divan 8y agoI wish more developers could do "investigation" like this for a new languages they learn. For me, the main difference between Go's way of handling language and the rest of mainstream languages is that it makes error handling unmagical. It literally says – errors are just like any other return values. Let's say, if you have function `sqrt` and return a value, and then call this function – you probably is interested in this return value and should handle it somehow (or mute with `_`). Now, the same applies for errors - if function returns the error, you likely to think how to handle it – do something in place or propagate up the stack. There is also a cultural moment to this. As we mostly learn by examples, and most Go code has proper error checks (not equal to "proper error handling" but nevertheless), it makes newcomers to do the same as well, even while disagreeing with Go's way. I've heard from many devs that Go was the reason that made them appreciate proper error handling. And honestly, I feel this too, and I think the reason is that in Go it's too easy to "handle errors properly". I never had this feeling with languages with exceptions, where I had to read whole books (!) just to learn how to properly use them and be confident in the way how I handle errors. (this is just an example, not the spark to start return values vs exceptions battle, just in case)
- erik_seaberg 8y agoThe flipside of easy-to-learn is there's no payoff for getting better with the language. Your code will always be exactly as tedious as novices' code because they'd rather conserve compiler cycles than spend them to amplify programmers' work.
- dullgiulio 8y agoA cursory look at the article, shows that the most important observation about error handling in Go is missing. Errors should be "decorated" (wrapped, contextualized...) in 99% of the cases. In the end you get errors that describe step by step what your program tried to do and why it failed, for example: * could not load profile: could not open file: permission denied. * could not download profile image: could not open URL: HTTP GET failed: network is down. This has many advantages: 1. Much more readable than stack traces (especially if they include source file and line information or exception class names: users don't care about those.) 2. Errors are still easy to grep in code to work out the program flow (the stack trace, basically.) 3. When reading the code, you can see from the error context strings what the code is actually doing. Basically it serves a function of comments and (unlike comments) error strings remain up to date. It is definitely verbose, especially the not equal nil part, as it's a result of Go attempt not to have special cases. Also it's a pity that errors can be silently ignored: maybe Go2 could be stricter here. Overall, I think this is one of the best approaches at error handling.
- Groxx 8y agoIn my experience, this just becomes arbitrarily close to "re-implement your stack trace by hand with space-delimited words instead of camelCaseFunctionNamesOrWhatever". I'll overwhelmingly prefer an always-correct stacktrace over a hand-recreated one that sometimes collapses multiple branches into a single ambiguous on. At least then the devs can help me when it fails. And stack traces and concatenated strings are in no way appropriate error responses for humans unless you're expecting them to be able to navigate the source code, so neither does anything for the "provide a helpful error message for non-programmers" problem. --- this is why stuff like https://github.com/pkg/errors https://github.com/pkg/errors exists. wrap at the deepest level / where the error originates, and it's relatively rare that you need to add context at higher levels. If you want user-friendly errors, you need something dramatically more sophisticated.
- jy3 8y agoJust using WithStack() from "github.com/pkg/errors" on any error that originates from outside my repository has been my go-to rule for any Go project. It has never disappointed.
- arendtio 8y agoFor everybody who is interested in improving his error handling skills: https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully https://dave.cheney.net/2016/04/27/dont-just-check-errors-ha...
- macrael 8y agoWhat is not covered here, and what I'm still searching for a good pattern for, is being able to return different errors depending on the type of failure. Suppose you have a function that fetches a model from your database. It can return an error if the given user doesn't have permission to fetch this model, or it can return an error if your db connection barfs for some reason. The calling function needs to be able to differentiate between the two errors. Most of what I've read on the subject makes it seem like people prefer to only ever check if err != nil. The two options I've seen in the wild are: 1. Create a constant for a given error, like: var ErrFetchForbidden = errors.New("FETCH_FORBIDDEN") Then the calling function can do: if err == ErrFetchForbidden { return 403 } else if err == ErrFetchNotFound { return 404 } else { return 500 } 2. Create a custom type for your error like so: type ErrFetchForbidden string this has the benefit that the errorer can put more specific info into the error besides the Error() string. var err ErrFetchForbidden = "error retrieving the user object" return err and then the caller can switch on type switch v := err.(type) { case ErrFetchForbidden: return 403 case ErrFetchNotFound: return 404 default: return 500 } We've gone with option 2 for now, (wrapping them with the pkg/errors package) because it seems simpler. Anyone else have good patterns for handling this?
- cube2222 8y agoThere's another one I often use: Create a custom error type, for example DB Error: type DBError struct { Temporary bool NetworkBased bool Cause error } Now you can provide functions like IsTemporary(err). Otherwise, you can use 2# with a twist, instead of matching on a type, you can do: switch { case isErrFetchForbidden(err): case isErrFetchNotFound(err): } or even: IsBadRequest(err) IsInternal(err) IsTimeout(err)
- macrael 8y agoSo you then define your function to return the type DBError instead of a generic err type. That makes sense to me but for some reason some of the stuff I've suggests that just returning err is more go-like.
- mdwhatcott 8y ago> The other bit of good news is that you can't unknowingly ignore a returned error, like you can with an unchecked exception. The compiler will force you at a minimum to declare the error as _, and tools like errcheck do a good job of keeping you honest. Actually, we unknowingly ignore returned errors much more often than we think, like when we call a function and opt out of assigning any of the return values to variables. Consider this function, which returns a single value (being an error). func Failure() error {...} You can always choose to call an error-returning function without declaring any placeholder (`_`): Failure() There are several commonly used functions that return errors that are regularly ignored. How about `io.Writer`? writer.Write([]byte("Hello")) // returns (n int, err error) It's quite common to call that function without feeling a need to check on the bytes written or a possible error. Or, consider whether you consistently check the return values of `fmt.Println()`, which also returns `(n int, err error)`...
- ecnahc515 8y agoerrcheck is a good tool to help with this.
- Sidnicious 8y agoThe first time I used Go on a big project, error handling bugged me enough that I wrote a package that let me use errors like exceptions: https://github.com/s4y/go-exc https://github.com/s4y/go-exc I’m not sure if I’d use it again today, but it was a fun exercise.