
On the uses and misuses of panics in Go - r4um
https://eli.thegreenplace.net/2018/on-the-uses-and-misuses-of-panics-in-go/
======
Animats
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.

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

~~~
TheCoelacanth
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/](https://docs.rs/error-
chain/0.12.0/error_chain/)

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

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

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

~~~
munificent
_> 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](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.

~~~
dilap
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.)

~~~
atombender
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
      }

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

------
ddtaylor
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](https://i.imgur.com/3l3NMrv.jpg)

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

------
twic
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](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.

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

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

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

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

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

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

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

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

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

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

~~~
Rusky
> 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...](http://www.open-
std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r0.pdf) (Though also note that an
Either/Maybe/etc-style syntax could also be implemented as unwinding if you
wanted to...)

------
knorker
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)

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

