9 ms·
On the uses and misuses of panics in Go
- andreareina 8y agoI really dig the common lisp condition/restart system, where the callee defines different ways to recover from a failure, and the caller chooses which applies; or the caller can do their own thing with `unwind-protect`.
- taeric 8y agoThe condition/restart system truly is close to magical. I've never used a system that was built on them, though. :( I think the problem is that it really is geared to empower the operator of the system. That is, it is seen as where you give the operator of the system a chance to choose what to do next. Much of the code I write is assuming the operator can start things, and that is about it. They are not sitting at a terminal waiting to make a decision about something. Too many transactions happening for that to be even remotely feasible.
- ChrisSD 8y ago> Go has a unique approach to error handling, with a combination of explicit error values and an exception-like panic mechanism. I don't think that's very unique in itself, although Go's particular quirks might be.
- endymi0n 8y agoAt least around the mainstream languages, I find the try/catch construct (where errors are treated very differently than return codes) much more prevalent. C‘s hacky -1 style perverted error codes don‘t really count for me. And where you have error return values by convention (like Node.JS), these are on top of this, so calling library code isn‘t guaranteed to not error out synchronously as well.
- TheDong 8y agoI guess you haven't used rust, or ocaml, or haskell. In those cases, errors are even more like normal values because they're returned using generic types (Optional/Maybe, Result/Either), not using hacky multiple return stuff. For example, in go you might have a function that returns (string, error), but you can't define a method on that type that was returned, so e.g. you can't have: func (tuple (string, error)) OrElse(val string) string { if tuple.1 == nil { return tuple.0 } else { return val } } This would let you right such code as: hostname := os.Hostname().OrElse("localhost") On the other hand, in rust or haskell you'd return a generic sum type which is either an error or a value and which can be treated as an actually normal value. This leads to the languages I mentioned above having the ability to have much cleaner and more concise error handling than go. The fact that go has multiple returns, not tuples or sum types, results in its errors being really awkward to use values, among their other flaws.
- golangnews 8y agoThis (optional types) is one thing I'd really like them to explore for go 2.
- kornish 8y agoThe generalization (sum types) would be even more powerful. Fingers crossed.
- qalmakka 8y agoYes, a fix to the mess error handling in Go is is long overdue, and I find Rust and Haskell approach one of the most effective ones, clear to understand and not too verbose, and yet powerful and relatively free of the mess exceptions are in other languages. I sincerely hope the Go authors will give sum types a shot in future releases of the languages, because they are so comically powerful that implementing them just feels like the right thing to do.
- majewsky 8y ago> hostname := os.Hostname().OrElse("localhost") You could do the following: type maybeString struct { val string err error } func MaybeString(val string, err error) maybeString { return maybeString{val, err} } func (m maybeString) OrElse(defaultValue string) string { //...as above... } Then you can do: hostname := MaybeString(os.Hostname()).OrElse("localhost") Of course, this is not practical on a large scale since you don't have generic types and thus would need to `go generate` one instance of the above for each MaybeXXX type. At this point, you're probably better off just writing hostname, err := os.Hostname() if err != nil { hostname = "localhost" }
- deleted 8y ago[deleted]
- ddtaylor 8y agoThis post reminds me of a scene from That 70's Show where the wife walks up to the husband and asks "do you think I am smart?" and replies "oh, we're gonna fight today" https://i.imgur.com/3l3NMrv.jpg https://i.imgur.com/3l3NMrv.jpg
- eliben 8y agoIn what way does the post remind you of that scene?
- ilovecaching 8y agoIMO Go's error handling is a flop. I constantly see code with the same verbose error checking that simply shoves the error further up the stack. No better than try/catch. Maybe and Either are much more elegant solutions that allow errors to be checked when values are needed rather than when they are created. I also really like monadic catch/throw. Panic is an anti-pattern, I think, and seems oddly placed as I think the article was illustrating. Once again, these types of failures would be easier to represent with a richer type system.
- platinumrad 8y agoI also prefer the option monad, etc. over returning (result, result..., err) tuples but terseness of error handling is as much a property of the language (i.e. operators, features, etc.) as it is a property of the type system. Consider the "try!/?" pseudo-macro from Rust. If Go had macros, there would be nothing preventing something similar from being implemented.
- majewsky 8y agoIf Go had macros, it wouldn't have been as successful as it is. The appeal of Go is that it's both very readable and very writable, even if verbose.
- bbatha 8y agoGo already has code generation built into the standard tooling. It also has reflection which is arguably worse because the generated code is totally opaque. Good macros would cover most of the uses cases for the tools in a more cohesive and more tooling friendly way.
- cesarb 8y ago> Consider the "try!/?" pseudo-macro from Rust. On Rust, try! is not a pseudo-macro, it's an actual macro (which could be defined by anyone, since it uses only the public API for Result and From). Go to https://doc.rust-lang.org/std/macro.try.html https://doc.rust-lang.org/std/macro.try.html and click on the [src] link at the top to see its current definition.
- cygned 8y agoI cannot value if it is good or bad, but Go’s error handlig is really very verbose. On the one hand, it makes it quite easy to follow control flow but on the other hand it’s annoying to see an if err != nil on like every other line.
- tshannon 8y agoYep, which really explains why Go went with it. The Go authors have stated that it's designed for code to be read more than it's written, so they add verbosity in favor of clarity.
- usefulcat 8y agoWhether that verbosity improves 'clarity' depends on what exactly you're most interested in seeing. If you primarily value being able to see the control flow for error conditions, then it does indeed improve 'clarity'. However, regardless of what you value, that increased 'clarity' necessarily comes at the expense of the readability of the non-error control flow.
- tapirl 8y agonecessary "if err == nil"s never do bad. I did found some Go code contains many unnecessary "if err == nil"s, however I think this is not the mistake of Go, it is just thant some Go programmers misuse them.
- deleted 8y ago[deleted]
- deleted 8y ago[deleted]
- Animats 8y agoI've had this argument before, regarding both Go and Rust. The history goes like this: 1. Exceptions suck! Look at the mess in Java and C++. In our new language we're not going to have exceptions. We get rid of all that complicated "unwinding" stuff, too. 2. Look at all that error handling code in our new language! 2/3 of the lines are about errors. For the really bad ones, we'll have "panic", and you can't recover from a panic. 3. We need to recover from panics in production! So we'll add a catch or recover mechanism. 4. When we recover, everything is messed up and we get memory leaks and stuff that's not closed! We need proper unwinding so files and connections get closed. 5. OK, now we have all the machinery for exceptions, but don't use them! Exceptions are bad! Except where cool people use them. Besides, their syntax sucks and it's hard to send useful info with a panic. 6. Oh, hell, put real exception support in the language. Go and Rust went through each of those steps and are now at step 5.
- zzzcpan 8y agoI see "real" exception support as an attempt to solve deficiency of resource management on errors in a shared memory model. But it never actually solves it, only puts a burden on people to manually deal with it (sort of like manual memory management) and introduces a lot of accidental complexity into everything it touches. Such problems are way better solvable with killable isolated processes and message passing (actor model). Otherwise discouraging use of panics and primitive support for exceptions is good enough. It helps avoid too much manual error propagation in certain rare cases. Real exceptions are still not a solution though.
- candiodari 8y ago> and introduces a lot of accidental complexity into everything it touches No it doesn't. That complexity is there. What you mean to say is that it makes it more obvious (and far easier to debug) when things go wrong. Manual error handling ... let's just simplify that to "igoring all errors", and if you aren't convinced, looking at a few Go repositories on github will drive the point home. So all "no exception system" really does is switch the default from "crash on error" to "ignore error". In C, before Java, this caused quite a few disasters with VERY bad error behavior (like silently deleting/overwriting all data, or starting a new data file, which you now get to merge manually with the one it ignored, for instance) ... in fact unix C tools are still (and, imho, justifiably) famous for their "nuke the whole world upon encountering a slight syntax issue" behaviors. Besides, the Go authors' own tools usually crash (log.Fatalf), but without stacktrace or unwind, on 90% of errors. That's, of course, even worse than exceptions crashing out of the program in several ways: 1) You have no idea what happened. We're back to grepping the source code for the last string it printed and praying you find something. Stacktraces ! GIVE ME BACK MY STACKTRACES. 2) Cleanup does not happen (!!!!!) with log.Fatalf. I repeat: NO CLEANUP WILL HAPPEN WHEN CALLING log.Fatalf. DO NOT USE log.Fatalf if your program has done anything at all !!!!!!!! The go authors themselves, especially should have gone through the exercise of, at EVERY error check, figure out what needs unwinding (file closing, ...) and then doing the same at higher levels of the stack as well. Needless to say, NOBODY, not even Rob Pike himself, has done that. As far as I can tell, not even once. 3) we all know how these bugs get fixed. By changing "crash out" to ignore. And frankly, given point 2, I will actually call it an improvement.
- twic 8y agoGo seems like a bad venue to compare the value- and jump- based approaches to error handling, because its value-based error handling is so poor that it ends up pushing people towards jump-based handling. I'd be more interested in reading about this in Haskell or Rust or whatever. I also don't think anyone should be allowed to write, or read, about error handling unless they've read and digested 'The Error Model': https://news.ycombinator.com/item?id=11054912 https://news.ycombinator.com/item?id=11054912 And if they have, they should be required to say so at the start of their article, so i know i'm not wasting my time reading it! :) I would say that one of the lessons from the evolutions of errors in Rust is a sort of negative-space analogue of Animats' story - that with sufficient syntactic sugar, value-based error handling behaves pretty much the same as jump-based error handling, and that we shouldn't think of them as alternatives, but as directions on a continuum. A bit like how we realised that garbage collection and reference counting are on a continuum: https://news.ycombinator.com/item?id=14823054 https://news.ycombinator.com/item?id=14823054 The most interesting thing i've read on this recently is the proposal for "Herbceptions" in C++: https://news.ycombinator.com/item?id=17059297 https://news.ycombinator.com/item?id=17059297 This defines a do-over of exceptions in C++ in such a way that they can be implemented as return values, and lose the usual disadvantages of exceptions. Particularly with the optional bit in section 4.5, where you have to explicitly (but tersely) mark calls to functions which can throw, so you never have exceptions unexpectedly appearing out of nowhere.
- iainmerrick 8y agoSomething that seems to get forgotten in these discussions is that there’s a big difference at runtime between exceptions and error flag return values. With error flags there’s a lot of code that runs all the time (although simple code with very predictable branches). A cycle or two in the common case, maybe tens of cycles for a branch mispredict in the error case. Whereas exceptions (in the C++/Java style) are free when they don’t fire, but very expensive when they do -- hundreds or thousands of cycles to parse exception tables and unwind the stack. Won’t anyone think of the runtime? :) I tend to think exceptions are better language design since they give more freedom to use different implementation strategies. But of course that freedom isn’t really useful unless you know the programmer’s expectations about how often errors are likely to occur, and as others have noted that’s really the hard part of the problem.
- Rusky 8y ago> Whereas exceptions (in the C++/Java style) are free when they don’t fire, but very expensive when they do -- hundreds or thousands of cycles to parse exception tables and unwind the stack. This isn't the whole story. The unwind control flow edges inhibit optimizations and increase code size. This is part of why C++ added `noexcept`. On the implementation freedom side of things, I am inclined to agree. For example, C++ is considering reimplementing exceptions as return values under the hood: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r0.pdf http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p070... (Though also note that an Either/Maybe/etc-style syntax could also be implemented as unwinding if you wanted to...)
- knorker 8y agoGo's error handling is very annoying, or quirky, or inconsistent. I can't find the right word. On the one hand you have to boilerplate EVERYTHING, check for errors ALL THE TIME. The benefit being "what's there is there. No magic data flows". On the other hand you also have to write exception-safe code (BUT DON'T USE EXCEPTIONS!… at least not on purpose…). Words exist to have meaning, and there's a precise and accurate word for what Go calls "panics", and that word is exceptions. But most Go programs code as-if the language doesn't have exceptions, and thus don't write exception-safe code. (and don't say that a panic should kill the task. fmt printer functions and HTTP handlers have their panics swallowed, so that's not a feasible assumption)
- axilmar 8y agoHaven't we realized yet that programs do not have a single flow of execution? each program has a few correct paths, and a few paths resulting from error. Our languages so far pretend there is one correct path, and that the error paths are almost non-existence, or a nuesance at best. In my opinion, what languages should do is the following, when an error happens: the program stops with an error report. There shouldn't be any errors caught ever. A faulty program should not be restarted; it should be fixed.