Hacker News new | comments | show | ask | jobs | submit login
On the uses and misuses of panics in Go (thegreenplace.net)
98 points by r4um 70 days ago | hide | past | web | favorite | 73 comments



I'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.


This is not the history of Rust, and it's rather disingenuous of you to pretend that it is.

Rust has featured unwinding in some form or another since its inception.

It has never been the position of the Rust developers that "exceptions suck", only that the failure mode of code that is not exception-safe is especially severe for low-level languages, especially ones that intend to be memory-safe. Rust's aversion to exceptions is not intended as a condemnation of their use in languages that make different tradeoffs.

The only way to "catch" a panic in Rust is via an obscure stdlib API whose purpose is to prevent unwinding from crossing FFI boundaries when Rust code is called from another language (because most host languages consider unwinding across FFI to be undefined behavior). I have read a lot of Rust code; never have I seen anyone attempt to abuse this mechanism as a replacement for general exception handling (which wouldn't even work in general, because Rust code can (and often is) compiled without the ability to unwind at all).

I implore you to actually make an attempt to learn Rust, so that in the future you may be more informed when commenting about it.


In Rust (I don't know about Go), there's a sharp line between "panic" and other error handling: the unwinding on a "panic" is optional. A program or library might be compiled with "panic=unwind", or it might be compiled with "panic=abort" where a panic kills the whole process instead of trying to unwind. This means "panic" can't be used for normal error handling, only for "can't happen" errors like an out-of-bounds index into a slice.

In your categorization, Rust with "panic=abort" would be at step 2, not 5 (and it's used in production that way, for instance AFAIK Firefox uses "panic=abort"). And interestingly, not unwinding on panic in Rust is newer than unwinding on panic: it was added only on Rust 1.10 (https://blog.rust-lang.org/2016/07/07/Rust-1.10.html). So Rust seems to be following your list of steps somewhat backwards.


Denial. Exceptions are bad and no one needs them.

Anger. Ugh, we've got manual error-handling code everywhere. It's awful!

Bargaining. What if we just add a couple of really limited features to handle really exceptional errors...

Depression. Oh, God, now we have all of the trouble of exceptions along with our previous weird half-baked error-handling mechanism.

Acceptance. Well, we've got too many users, so now we're stuck with it. At least people seem to be able to muddle through anyway.


Preferences for exceptions vs error codes is usually determined by the desire (or not) to have a control flow edge specific to handling the error case, which in turn depends on what kind of code the programmer is writing.

Exceptions are optimized for unwinding (i.e. abort, not handling); error values are optimized for handling; while monadic Maybe / Either constructions try to have it both ways, and are approximately equivalent to checked exceptions with more cumbersome syntax, depending on how good the language is with lambdas. The heavyweight syntax reminds you of the error's existence.

The thing is, while some errors conditions can easily be put in the exceptional category (panic / abort), and others can easily be put in the "you really should handle this, it's not unusual" category, there's a large space in the middle where the preferred choice is heavily dependent on context and the program being written.

Business code called from a server loop almost never cares about exceptions; unwind all the way to the top, rolling back transactions along the way, log, and loop back around to handle the next request. Networking code in a client or server almost always wants to handle errors from the network; network errors are not unusual.

It's almost like the choice should be up to the user at the call site. Unfortunately the decision needs to be global, because error propagate. Errors are peculiar like this: they violate abstraction boundaries. The failure modes of a module are a function of how it's implemented. If the module interface is designed to be polymorphic, wrapping up error conditions in abstract values makes them almost unhandleable - hiding the cause means you can't take action. Thus errors are a leading cause of leaky abstractions.

Erlang was not originally designed for concurrency; it was designed for fault tolerance. That resilience was achieved not by handling errors, but by tearing down the faulting process, and letting a supervisor set things back up again. Killing the process also means throwing away all the data it had in flight, and because data isn't shared, the risk of corruption is much lower, and semantically the tear down looks a lot like a transaction rolled back.

Erlang appears to be pretty good at achieving its goal of fault tolerance. Does it look more like error codes or exception handling? A lot more like exception handling if you ask me. But is all the rest of it needed to follow through properly?


Rust has had unwinding from the very beginning. It was even in the private design docs. It also always had recover, though originally only at thread boundaries. Result came after unwinding was implemented.

I had to implement part of the unwinding, so I'd like to think I know what I'm talking about.


I don't recover from panics in production Go and none of the code I depend on panics to handle errors.

Errors are handled the long and boring way inline and I like it that way. (I would like better tools for making better errors, but not for pretending errors don't exist.) When I work in C, I do the same.

Your charactization of Go as I know it is wrong.


I prefer the boring way as well and it should stay like this.

My gut feeling would be that otherwise, instead of handling errors locally, you “sprinkle” your code with “try” keywords. Afterwards you want to distinguish between exception types and eventually you end up switching over these types in a big switch higher up.


Go and Rust have very different mechanisms, due to very different world visions.

1 - Exceptions do suck. It's obvious in Haskell where there are better tools but sometimes you aren't allowed to use them.

2 - Rust thus goes into: well, let's take those better mechanisms and allow them everywhere. Go goes into: well, let's create some alternative mechanism like the ones most people are used to.

3 - Rust then goes into: oops, our error handling does not stack well so people are just panicking everywhere (what actually requires more code than proper handling). Go goes into: oops our error handling requires 2 times as much code as the actual program, so people are just panicking everywhere.

4 - Rust will eventually solve its crisis by creating something equivalent to "do" notation. I don't really think there's any solution for Go, but I do expect them to create some low level exception system so they can keep the language simple - maybe this time they'll copy VB instead of C.

By the subject of your point #6, Rust's mechanism is strictly more powerful than exceptions. It can do anything exceptions do, and more. Go's evidently is not.


I would describe Rust's error handling crisis more as:

1. I'll just handle all of the errors by having my functions return Result<T, E> where E is some type that encapsulates all of different error conditions that can happen in my function.

2. Function one returns Result<T1, E1> and function two returns Result<T2, E2>. What type do I use for a function that calls both and just uses `?` to defer handling on errors from either function? Guess I either have to use Box<Error> and do a bunch of ugly downcasting whenever I want to handle an error or I have to write a bunch of code to define another error type E3 which combines E1 and E2 and I have to implement From<E1> and From<E2> so now I have more code just for defining error types and conversions than I have for actually doing anything.

3. Create complicated macros like error_chain[1] or failure[2] to try to cut down on boilerplate in #2.

[1] https://docs.rs/error-chain/0.12.0/error_chain/

[2] https://docs.rs/failure/0.1.2/failure/


> Guess I either have to use Box<Error> and do a bunch of ugly downcasting ... or I have to write a bunch of code to define another error type...

Yes, that's the monadic equivalent to `catch(E1 e) {handle e}; catch(E2 e) {handle e}; catch(E3 e) {handle e}...`

On practice things get all over the place, with some times the exception being more concise and other times they being way more verbose. But the problem to be solved is not verbosity, it's with implicit code catching you by surprise, with too generic handlers on code that can't reasonably handle an error, and some issues with how errors interfere with code composition.


> Rust will eventually solve its crisis by creating something equivalent to "do" notation.

This is... not at all true? Rust already has the `?` operator, and there's no indication of this being replaced with do notation or anything like it.


> Rust then goes into: oops, our error handling does not stack well so people are just panicking everywhere

Do you have any citation for this? That’s not my experience at all.


My experience is that result passing in `.then` makes code brittler.

The part about people panicking everywhere is hyperbole (on the mood of the article). I've seen more panics than it makes sense and resorted to them too on disposable code, even they being more up-front work than handling it correctly.


> Go goes into: oops our error handling requires 2 times as much code as the actual program, so people are just panicking everywhere.

That sounds more like laziness on the part of the programmer, rather than the language design.


Any language that doesn't acknowledge programmer laziness is faulty. All programmers will have times where they do only what is easiest to do right then. Sometimes you just need to get the happy path working, damn everything else.

Good languages account for this, and make the best choice the easiest choice whenever possible. If the best choice is twice as much work, there will be a lot times when the language encourages the programmer to do something else. I don't consider this a fault on the part of the programmer, I consider a fault on the part of the language.


Interesting alternate viewpoint!


>All programmers

I bet this generalization is wrong


All generalizations are wrong. Including this one.


Joe Duffy's series on the Midori system goes into great detail about the problems in various error-handling scenarios and how they went about dealing with them: http://joeduffyblog.com/2016/02/07/the-error-model/


I 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.


> 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.


> Go and Rust went through each of those steps and are now at step 5.

panic/recover is real exception support already. In principle, panic/recover and exception/catch are the same thing. Go and Rust support exception since the first days of their formal releases. It is just that they don't recommend to abuse the exception mechanisms.


It's still way nicer to read compared to the clusterfuck that java is.


Agreed. I much prefer the style that Anders H. went with in C# of unchecked exceptions and the recommendation to have a single “catcher” at the top of a main event handler/loop spot in the code.

Rather than catch littered everywhere, there is just a top level thing to do something like log a “well it died, cuz <<reason>>, thought you should know (moving on to the next thing)”.


That's what erlang does too. "let it crash".


Seems like step five is a reasonable place to stop, and I don't know any languages that eventually got to step 6 and are useful or fun to program in as rust. Exceptions are really not a great way to think about error handling.


> Exceptions are really not a great way to think about error handling.

Why not? They let programmers concentrate error-handling in specific points of execution while taking care of every single resource cleanup task the programmer sers fit to perform.

Is there any better way to tackle this problem and keep the source code readable?


Swift?


Come on, you can get kubernetes to recover your process gracefully. I haven't bothered to figure it out, but I know the solution exists.


IMO 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.


> I constantly see code with the same verbose error checking that simply shoves the error further up the stack.

I found this tragicomically illuminating:

https://anvaka.github.io/common-words/#?lang=go

The four most common words in Go programs are literally: "if", "err", "return", "nil". If that doesn't tell you the designers missed the boat on something, I don't know what does.

What I find frustrating is the attitude that went along with it. I think it's reasonable to say, "Yeah, error-handling is important but we think Java-style exceptions have too many problems. We don't want to do that, but we're not sure what a better alternative is yet. It's a hard problem."

But instead, it's: "Our proposal instead ties the handling to a function - a dying function - and thereby, deliberately, makes it harder to use. ... If you're already worrying about discriminating different kinds of panics, you've lost sight of the ball."

It's never the language being wrong, you're just using it wrong.

Good error-handling is hard. Users want to be forced to handle errors that they want to ensure are handled, but not annoyed by errors they don't care about. Telling which from which is highly contextual, and the language rarely has that knowledge.


I don't know. I find the distinction of "panic if it's a bug, return err if it's a normal condition" to work pretty well.

It's much easier for me to write robust server software in Go than in either Java or Python -- because I don't constantly have to be thinking, "oh, is this thing going to suddenly unexpectedly throw?"

Basically, exceptions for exceptional conditions is good. But Java and Python use exceptions for normal conditions, which is cumbersome and annoying.

So for me, Go's approach to error handling is an improvement over just exceptions. The fact that it's verbose is fine if maybe not ideal.

(My vague understanding (just from reading, not using) is that's rusts is similar but more ergonomic. Ditto for Zig.)


The problem in Go isn't that the semantics are wrong, it's that the error handling is so prevalent that it starts obscures your logic. I'm writing an app right now where almost every piece of high-level code is:

  if err := doThis(); err != nil {
    return err
  }
  if err := doThat(); err != nil {
    return err
  }
  if err := doThisOtherThing(); err != nil {
    return err
  }
  // ...
The patterns in Go code are typically either (1) "call this and bail if error" or (2) "call this and bail if error, wrapping the error in a new error", with a sprinkling of (3) "if the error is io.EOF or whatever, do something else".

The #1 case is so dominant that Go really cries out for a built-in error-propagation mechanism.

The other problem with Go's error handling is the use of multi-value return types. I suspect Go's designers thought this was elegant, but it confuses things, because all error-returning functions return either a value or an error. Never both. It's not two return values in actuality, it's a sum type (aka product type or union). I'd take this over the current scheme:

  func GetFile() (io.Reader | error) { ... }
And then maybe:

  switch t := GetFile().(type) {
    case io.Reader: ...
    case error: return err
  }
which can have the following semantic sugar:

  r := try GetFile()
Rust has a macro that does something like the above, but the type system supports sum types and matching already.

Now you can do:

  r := try GetFile()
  count := try r.Read(buf)
instead of:

  r, err := GetFile()
  if err != nil {
    return err
  }
  count, err := r.Read(buf)
  if err != nil {
    return err
  }


agreed; there's real (potential) niceness in these ideas.


>The four most common words in Go programs are literally: "if", "err", "return", "nil". If that doesn't tell you the designers missed the boat on something, I don't know what does.

Are you trying to make a case for exceptions? If so, this isn't it. The "boat" the designers missed are Result types, or more generally "generics". I'm willing to bet most go function signatures are of f(...) (T, error). The if a,err := g(); err != nil boilerplate could be solved with Rust's `?` operator. For example:

    var exp int
    if a, err := compute(); err == nil {
        exp = a
    } else {
        return nil, err
    }
could be rewritten as

    exp := compute()?
Rust has `Result<T,E>` which makes this "simple". A `?` style operator in go would probably drastically reduce the mount of `err != nil` code in the wild.

At the same time, the code is still being shoved up the stack.


I'm not making a case for any specific language feature. I like exceptions, result types, and Icon's notion of failure. Just something that's better than what Go has today.


I too, have found error handling in Go to be quite good even if it is verbose. I like knowing where exactly an error has come from and that the language makes me think about how to handle them before continuing.

> ...but not annoyed by errors they don't care about. Telling which from which is highly contextual, and the language rarely has that knowledge.

Isn't this where the programmer's knowledge + a little thinking come into it? With experience you come to understand what kind of errors certain functions will return and then you spend a little time thinking about the context this error may appear. With this you determine whether the error has to be handled or can be safely ignored via a '_'.


With this you determine whether the error has to be handled or can be safely ignored via a '_'.

In my experience most of the time I want to neither explicitly handle the error nor ignore it. I want it to propagate up to a top-level handler that logs it and either fails the current operation or aborts the entire program.


The Icon programming language has a nice error handling style that reminds me of Go's local error handling but with less boilerplate. Functions can return success/failure along with the actual return value, like an implicit Maybe/Option type.

Converting the article's example code from Go:

  f, err := os.Open("file.txt")
  if err != nil {
    // handle error here
  }
to pseudo-Icon:

  if f := os.Open("file.txt") then
    read(f)
  else
    handle error here
https://en.wikipedia.org/wiki/Icon_(programming_language)


iirc it works like that in swift:

if let file = os.Open(file) { do sth } else { handle error }


Practically the verbosity isn't a big deal. It's not hard to type and you can always copy and paste.

More importantly, as a pattern it's pretty easy to read. The code and meaning is clear and you can follow it.

Maybe some syntax would be nice to shorten it, but it really doesn't bother me all that much. You get used too it.

Bigger problems are language warts which lead to bugs, and a nasty one has to do with interfaces and nil: https://golang.org/doc/faq#nil_error . Seems like it would be much better for them to fix that. (and there are attempts to do so in issues on GitHub)

FWIW the errors package is a nice way to further annotate errors: https://github.com/pkg/errors

And at least with distributed systems error handling is a more complicated affair. The boilerplate ends up not being so boilerplate when some errors should be ignored, some retried and some should result in a panic.

I guess there's an appeal to a well defined exception type hierarchy for something like that, though how that fits with a language like go isn't at all clear... in practice knowing the type hierarchy is also hard ... what's the difference between Error and Exception ends up being a common bug in python for example.

We treat these problems like they have obvious solutions, but they don't. Fixing them the "easy" way ends up turning Go into a very different language, and it will be a sad day when that happens.

Folks who enjoy working with Go will just fork a minimalist version, and the folks who complained won't use it anyway and will stick with rust or whatever. At least that's my sense of it.


I 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.


> 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 and click on the [src] link at the top to see its current definition.


I called it a pseudo-macro because while "try!" is a macro, its usage is discouraged in favor of "?", which is an operator.


If 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.


Go 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.


Isn't Go's error handling basically just a procedural version of Either? It's a tuple instead of a sum type and the Left and Right cases are flipped, but otherwise I thought the entire point was that you're dealing with values that you only need to worry about at access time.

Sure, it's not a proper functor, but for a procedurally-driven language design, I would think that the current design makes more sense for debugging.


There's a major downside to the multi-value returns compared to sum types: They can't be chained. You have to assign the two result values to two separate variables:

  s, err := readFile()
There's no way to build something like this:

  readFile().then(func(s string) { ... })
Well, you can build it by having readFile() return some object with a then(), but without generics you can't build anything remotely ergonomic here.

(Actually, Go has an interesting quirk where multi-value return values can be passed as arguments to a function, so this is allowed:

  func handleErrors(s string, err error) { ...}
  func readFile() (string, error) { ... }
  func main() {
    handleErrors(readFile())
  }
Unfortunately, there's no way to build a generic chaining mechanism with this, since you'd have to have a handleErrors() for every possible return type combination.)


You mean a bit like NaN in floating point math?

ie. code execution continues with the same flow, but any code that takes as input an error will return the same error.


I find NaN is mostly unhelpful on balance. It can help avoid crashes, but you get garbage results and it’s often hard to trace back to the underlying culprit. NaN means kicking the can down the road and failing as late as possible, but failing early means much easier debugging.

Something like a NaN (for arbitrary operations, not just floats) that could also give you a full backtrace would be very interesting -- a bit like Java’s nested exceptions.


This 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


In what way does the post remind you of that scene?


Go 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

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

The most interesting thing i've read on this recently is the proposal for "Herbceptions" in C++:

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.


I 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`.


The 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.


I 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.


Yep, 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.


Whether 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.


necessary "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.


> 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.


At 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.


I 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.


  > 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"
  }


This (optional types) is one thing I'd really like them to explore for go 2.


Yes, 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.


The generalization (sum types) would be even more powerful. Fingers crossed.


OCaml and Haskell do have exceptions, though.


The Go's way is an almost verbatim copy of the usual Bash and Perl error handlers. It's clearly based on C, with some improvements, but achieved almost the exact same format by coincidence.

(Both Bash and Perl would very likely have the same syntax as Go if they had tuples.)


Something 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.


> 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/p070... (Though also note that an Either/Maybe/etc-style syntax could also be implemented as unwinding if you wanted to...)


Go'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)


Haven'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.




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: