
Errors in Go: From denial to acceptance - progapandist
https://evilmartians.com/chronicles/errors-in-go-from-denial-to-acceptance
======
josteink
I’m sorry but to me this just looks like Stockholm syndrome all the way.

The approach suggested for Go 2 under bargaining is basically a reverse try-
catch at best and a Visual Basic “on error goto” at worst.

Even in their own attempt to explain why try catch is bad they effectively
admit it serves a common use-case: A common use-case not handled by Go no
less.

This lacking also puts the burden on the programmer which will have to
manually handle each and every error, by himself, everywhere, one by one.

Even worse are the priorities given in the language: checking for errors is
cumbersome, while ignoring errors is easy.

I’m willing to be proven wrong, but my immediate guess is that this in general
leads to more buggy code, not less.

~~~
sephware
Without taking sides on the issue, these are purposeful trade-offs that the Go
team makes. They are trying to avoid bloating the language and making it too
difficult for them to add optimizations or other features. And I think they
would argue that it makes ignoring errors harder, since you have to
intentionally discard the error value when it was given to you, which takes
conscious effort, rather than omitting it from the code completely like other
languages let you.

~~~
aikah
> these are purposeful trade-offs that the Go team makes.

Trade-offs for whom or with what in mind? the compiler or the programmer?

That's the "philosophical" difference between the Rust team and the Go team.

~~~
Skunkleton
Exactly. That is why Rust does so many more things, but Go compiles so fast.

Even with two wildly different approaches to language design, each has their
place, and their own set of drawbacks.

~~~
repolfx
It's a false dichotomy. Languages like Java or Kotlin compile fast and have
exceptions too.

~~~
mmirate
Because the JVM takes another tradeoff against Go and Rust: it has a runtime
interpreter+JIT, which causes even more performance overhead than Go's GC let
alone Rust which - at runtime - is effectively a zero-cost abstraction over C
and jemalloc.

~~~
repolfx
The JVM GCs generally have much lower overhead than Go's does. Go imposes
enormous collection costs on apps to try and drive latency so low. This is
widely acknowledged in discussions of the various GC tradeoffs.

~~~
mmirate
The GCs alone? Yes. The rest of their respective runtime architectures? Highly
doubtful. As GC-dependent as Go may be, it still does end up as native code.

But frankly, comparing Go to Java when things like Rust exist ... seems
analogous to comparing the z80 to the AVR when 32-bit ARM cores exist.

~~~
repolfx
Offtopic/meta: for some reason your post was dead. The comment at the end is
kind of pointless and flamey, Rust isn't _that_ good, but doesn't seem to
justify a post kill. I vouched for you to bring it back.

------
lloeki
The author seems to generalise his use case into a description of how to use
panic/recover as a bespoke exception mechanism. In that case this is still
bargaining.

> In imgproxy, I use this approach to stop image processing if the timeout is
> reached (see here, and here). _The goal is not to bother about returning
> timeout errors from each function_

(emphasis mine)

While I understand the pain, I do not like the train of thought. It is our
duty as responsible developers to _bother_ with error handling. Luckily this
is not a library but a "standalone application", not a "library", which is a
slightly different use case (e.g negroni has to recover from panics to log and
continue serving http requests).

The accepted solution looks like† a world of pain waiting to blow up in a
myriad of subtle corner cases (do you remember what happens exactly when
there's a panic in a defer that was called because of a panic, and in which
order deferred functions are called?) and is barely more readable. I've seen
much more interesting and obviously robust Go 1 patterns (that I can't find
right now) that e.g pass error handler functions around.

† At first sight. Maybe it's not when digging deeper, but that's not the
point: the most glorious thing to me when I read idiomatic Go code is that
basically everything is _boringly_ obvious. This is a clever hack and crosses
a threshold I'm not willing to go past in production-class code.

~~~
jrockway
I completely agree. I think people are shocked, at first, at how everything
can go wrong in ways they've never considered. Exceptions hide the complexity
by pretending that errors are exceptional things that will never happen to
YOU. The opposite is what's true, though, random errors will happen all the
time; networks drop packets, remote machines are slow, database transactions
fail because of other writers, etc. If you are aware of all the things that
can go wrong, you can handle them. If you treat them as "exceptions" rather
than the rule, then you will just write flaky software that needs constant
manual attention. Some people like this, I guess, but I'm personally not a
fan.

The syntax could be better (I do have err, !=, and nil keys on my keyboard, I
really do) and it would be nice to annotate errors and not lose the semantic
information (fmt.Errorf("trying foo: %v", err) throws away the specifics of
err beyond the result of err.Error()), but really... explicitly handling
errors after every function, even if it's just "return err" and punt to the
code one level up... is something you have to do in every language. Go just
front-loads it.

As for timeouts, I am not sure why you wouldn't just pass in a context, which
already has provisions for a deadline, explicit cancellation, and cancelling
further library calls. If you can cancel your operation when <-ctx.Done()
returns, then you can ensure that all the related work is cancelled. You don't
need a clever panic/recover for that. Just select { case <-workDone: ...; case
<-ctx.Done(): cancelWork(); return ctx.Err() }. I am not sure why people rely
on hacks when there's already a standard method built into the language.

~~~
candiodari
The problem with this attitude is that it ... it's just not true. C-style not
true.

C, like Go, convinces people they handle errors. And then, when you look into
what that actually means, correctly handling error cases for even trivial
problems ... you immediately find out "nope, you're not handling errors,
you're ignoring them". C, and Go are very sneaky that way.

1) one of the more common error cases is nil, closely followed by out of
memory and zero division ... which in Go causes a panic just like every other
language.

So your Go error handling in this case is _never going to be executed_. If you
think you can avoid exceptions, you're lying to yourself. Every last method
needs to be ready that a panic gets called at almost any point. Which ... is
exactly what the error system was supposed to fix.

2) "If you treat them as "exceptions" rather than the rule, then you will just
write flaky software ..."

This sounds nice until you look at Go sources on github. And you see WHY
they're not flaky. Do people handle errors more in Go sources than in Java ?
The reverse is true !

But Go software _is_ more stable. How can this happen ?

Well, Go defaults to ignoring errors and just continuing whereas Java (and
every sane language) defaults to aborting the program rather than letting it
run in an unknown state.

To put it more extreme, and more to the point Go just starts "sudo rm -Rf /"
when an error happens. The reality is that Go programs just start writing
things to the database based on wrong information when an error happens.

3) "As for timeouts, I am not sure why you wouldn't just pass in a context,
which already has provisions for a deadline, explicit cancellation, and
cancelling further library calls..."

Again, this sounds cool. You can recognize code that actually uses this
correctly.

Incorrect (in more ways than one):

    
    
      if _, err = f.Write(something); err != nil {
        return err
      }
    

More correct:

    
    
      var err error
      cont := true
      for retry := 0; cont; retry++ {
        if retry > max_retries {
          return fmt.Errorf("couldn't do X in max retries. Last error was: %v", err)
        }
    
        didit := make(chan bool)
        go func() {
          defer func() {
            if r := recover(); r != nil {
              err = fmt.Errorf("the call panicked: %v", r)
            }
            didit <- true;
          }
          n := 0
          for n < len(something) {
            i, err = f.Write(something);
            if err != nil {
              return
            }
            n += i
          }
        }
    
        select {
        case <-didit:
          // Ok, go to next statement
          if err == nil {
            cont = false
          }
        case <-ctx.Done():
          return fmt.Errorf("context signaled abort")
        case <-time.After(timeout):
        }
      }
    

Have you seen such a code style ... even once ?

And what will this do ?

    
    
      f.Write(something)
    

One of 5 things:

1) it may actually work (one hopes, the common case)

2) it may not write anything

3) it may PARTIALLY write what you asked to be written (including not writing
anything at all and returning EAGAIN, which is why the retry logic is not
really optional. Frankly you should have a higher max_retries, and ignoring
for the EAGAIN case to avoid some very rare circumstances)

4) it may panic

5a) it may block for a long time

5b) it may block forever (and even make your program unkillable. I mean, we've
all used NFS, right ?)

What will be your total amount of material to diagnose this problem when it
occurs in cases 1-3 ? Nothing whatsoever.

Case 4 ? A large collection of stacktraces. Thankfully usually with the
relevant one on top.

5a and 5b ? 100 stacktraces, when you finally kill it (or ... well Go
"supports hundreds of thousands goroutines"), one of which is relevant. Well
the truth is that it may actually have so many goroutines that it ... well I
managed to run out of diskspace once. But it's easily 10 megabytes and more in
a real webserver.

~~~
acdha
I was going to write about how often I saw the res, _ := … error handling punt
and wanted to confirm that lack of even a warning but
[https://play.golang.org/](https://play.golang.org/) really shows how deep the
cognitive dissonance goes. The very first example many people are going to see
doesn't even acknowledge the possibility of error handling.

That would be safe in a language which uses exceptions or something similar or
in a language where the type checker forces you to do something with the
return value but Go has this odd mix of learning from C and ignoring its most
important lessons and treats ignoring errors as less of a problem than an
unused import. Simply having an implied if-error-than-panic check any time
someone assigns a value which is never checked would go a long way towards
turning subtle hard-to-debug failures-at-a-distance into clear failures at the
source.

Edit: none of the examples on the golang.org homepage handle errors. You
appear to have to go into the fourth section of the language tour to find
error handling first being discussed 19 pages into the ”Methods and
interfaces” section to find error handling discussed at all and even after
that point I only saw a couple of subsequent lessons where this looked like a
thing which every working developer should be routinely handling (i.e. the web
crawler acknowledges that fetching network resources can fail).

~~~
lloeki
> the res, _ := … error handling punt and wanted to confirm that lack of even
> a warning

Warnings don't exist in go build, because warnings are ignored by people. IIRC
there are linters that _do_ highlight those if you care for it and want hints
about things to check.

There _are_ legitimate use cases of swallowing error return values†, and if
you throw warnings intros cases, then you can't tell the difference between a
legit warning and a legit use case that shouldn't warn.

> none of the examples on the golang.org homepage handle error. You appear to
> have to go into the fourth section of the language tour to find error
> handling first being discussed 19 pages into the ”Methods and interfaces”
> section

Skimming over the fact that this is a "Tour of Go", not a full-blown tutorial,
there is not a single call that would return an error before the page you
mentioned. There are ok return values for map fetches and type assertions
before that though, but they're just printed out, and the same pattern
continues afterwards. Those are focused code snippets to show features and get
the vibe of things. I certainly wouldn't expect from a tour (for any language)
to be the equivalent of _The Go Programming Language_ book, nor Effective
Go[0].

[0]:
[https://golang.org/doc/effective_go.html#errors](https://golang.org/doc/effective_go.html#errors)

† It happens that by design you may have the guarantee that e.g ParseInt will
work because you're under full control of the input type and/or value.

~~~
acdha
> Warnings don't exist in go build, because warnings are ignored by people.

Okay, make them errors - an unused import is an error and the risk is orders
of magnitude lower.

> There are legitimate use cases of swallowing error return values†, and if
> you throw warnings intros cases, then you can't tell the difference between
> a legit warning and a legit use case that shouldn't warn.

Nobody is saying there aren’t cases where you can legitimately ignore them. My
position is just that it should require intent rather than being the default:
you should have to access any error type at least once after assignment, even
if it’s just to intentionally ignore an expected error (which is also
important for confirming that the error you’re ignoring is the one you
expected and not something else).

Regarding examples, yes, there’s a balance of not overcrowding examples but
people retain early lessons for long periods of time. Since almost all non-
trivial Go programs work with things like files and networks, it should be
more prominent because the language repeats the C style approach of leaving
error handling up to programmer diligence and that means you need to develop
the habit from the beginning because the language doesn’t have any other
protections.

------
ansible
The error handling story in Go had started to bother me more and more over the
years. Not that it was _that_ bad, it just felt cumbersome.

And then I started learning Rust for embedded development. I basically love
the entire language, and there are very, very few design decisions I disagree
with.

The error handling story is much better there, and if they're making breaking
changes in Go 2.0, I'd suggest they look there for a better path forward.

> _Panic-Driven Error Handling_

I'd insert a gif of Vader at this point, from the end of Episode 3.

I strongly disagree with this, using 'panic' should be a last resort in most
situations, when the program has no reasonable way to recover from an error
condition. File not found, user error, etc. are all common occurrences which
should be handled by the normal mechanisms. And this goes double for library
code.

~~~
weberc2
I mostly agree with this (especially that panic should not be used for error
handling, but only for exceptional circumstances). I think checked enum style
errors are the way to go, but I also think that Go's approach suffices. A
popular criticism of Go's approach is that the compiler doesn't force you to
handle your errors, which is true, but I find that Go's culture suffices here
--the biggest complaint I have about Go's error handling is that you have to
use a library to get stack traces (which is also true for Rust) and stack
traces are simply very practical.

The other popular criticism of Go's error handling is that it's verbose, which
I wholly disagree with--the verbosity is such a negligible cost that I would
much prefer verbose error handling to the language complexity required to
elide them (especially if the proposed sugar required baking "errors" into the
language as opposed to treating them like any other data).

~~~
ansible
> _A popular criticism of Go 's approach is that the compiler doesn't force
> you to handle your errors, which is true, but I find that Go's culture
> suffices here..._

The Go culture does suffice, but the Rust culture is even stronger here.
Everyone is expected to use Result for something that can fail, which I like a
lot for consistency's sake.

> _The other popular criticism of Go 's error handling is that it's verbose,
> which I wholly disagree with--the verbosity is such a negligible cost that I
> would much prefer verbose error handling to the language complexity required
> to elide them (especially if the proposed sugar required baking "errors"
> into the language as opposed to treating them like any other data)._

Six months ago I would have agreed with you. And then I learned about error
propagation using the question mark operator in Rust:

[https://doc.rust-lang.org/book/second-
edition/ch09-02-recove...](https://doc.rust-lang.org/book/second-
edition/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-
propagating-errors-the--operator)

Arguments can be made about creating custom error messages at each call level,
but is that really necessary most of the time?

~~~
jerf
"And then I learned about error propagation using the question mark operator
in Rust:... Arguments can be made about creating custom error messages at each
call level, but is that really necessary most of the time?"

Actually, one of the things that has prevented Go from "simply" or "just"
solving the error propagation problem is precisely the recognition that in
real code,

    
    
        if err != nil {
            return err
        }
    

becomes problematic at scale, because it obscures the history of the error.
Stack traces are great for humans, but not terribly useful for programmatic
interpretation. Personally, I still tend to use "if err != nil { return err }"
as my sort of default sketch implementation, but I find it is very common for
many of those to grow something else before the program ships, and I'm still
not all that great or consistent about layering errors properly anyhow.

I'd suggest to a lot of people that it may be worth reading or re-reading the
Go 2 error proposal [1], and to try to come at it with some relatively fresh
eyes, rather than reading it for "does this precisely conform to my
preconceived notions about what error handling should be?" (Another
interesting aspect is around providing some base API for errors, and
processing composite errors, rather than expecting end-users to do all the
definition [2]. Personally I consider both important.) There is some
legitimately interesting discussion and work around what to do, based on
waiting to gather a lot of data about error handling before just slamming a
solution into place, moreso than may meet the eye. I can't speak to Rust's
solution in practice, but having used Haskell for a long time with union types
for errors, I can say they are hardly error-handling nirvana there. If I'm
reading the Rust documentation correctly, they're going to have the same
problems in Rust as they do in Haskell.

[1]:
[https://go.googlesource.com/proposal/+/master/design/go2draf...](https://go.googlesource.com/proposal/+/master/design/go2draft-
error-handling-overview.md)

[2]:
[https://go.googlesource.com/proposal/+/master/design/go2draf...](https://go.googlesource.com/proposal/+/master/design/go2draft-
error-values-overview.md)

~~~
erik_seaberg

      if err != nil {
          return nil, errors.Wrap(err, "bad stuff during foo")
      }
    

looks pretty handy, I just want to tell the compiler to _infer that everywhere
by default_ , because writing it over and over is not only a waste of time but
actively makes all code harder to read.

~~~
jerf
That's part of what's going into the new error proposal. (Although it's not
going to be at the global level, but at the function level. "At the global
level" becomes tricky if you try to actually spec it out; it's easy enough to
just slap some designs down, but ones that compose well aren't so easy.)

------
pilif
_> Nothing explodes, the code keeps running_

in my book, this is not a good thing but a very bad thing.

If I'm a library function somewhere all the way down the stack and I'm at a
point where something about the state I'm working on isn't up to the
specification I expect them to be, then I'd prefer the safety of blowing up.

I don't want to be responsible for my callers eventually ignoring my error
code and happily churning along thinking that I have fulfilled my promise and
enacted the side-effect I promised to have.

If something else, far removed from me depends on me to having had the side
effect, _it_ might blow up. Or it might write corrupt data. Suddenly a thing
that went wrong at one place blows up at a totally different place which makes
it incredibly hard to debug.

And if the world is burning, "hard to debug" is about the least wanted
property a problem could have.

Oh no. If the world is burning for me, then I absolutely do not want to
continue.

But I also do not want to abort the daemon process I'm being hosted in because
that would mean affecting other threads of execution (be it threads,
coroutines or whatever else) where the world might be in a totally acceptable
state.

Calling `exit` in a library function is outright rude.

If I get to `throw`, I can make absolutely sure that I made it clear to my
caller or my caller's caller that I panicked while there's still a chance for
my parent daemon to not die but handle my panic cleanly. Or, my caller, or its
caller was prepared for me failing and can chose another way out.

But by throwing I can raise a very strong signal that something is wrong and I
have the guarantee that if I'm being ignored, then I _will_ kill my parent
which is totally their fault for ignoring me and it will be _very_ obvious
what happened, why it happened, and, above all, _where_ it happened.

If I'm an engineer wearing my devops hat, I'd _much_ rather debug an uncaught
exception causing a stack trace to be logged in sentry.io (or whatever else
you use. I'm a very happy sentry customer, but your might have your own tool)
rather than corrupted data that might have been corrupted months ago.

~~~
cle
> in my book, this is not a good thing but a very bad thing.

Your whole reply is a straw man. The author is claiming that nothing explodes
b/c the error is correctly handled, not b/c it was silently swallowed.

I've seen so many bugs caused by exceptions being silently swallowed in other
threads. Exceptions are not a silver bullet here, and you end up having to
pass errors as values across call stacks anyway.

~~~
TimJYoung
"Your whole reply is a straw man. The author is claiming that nothing explodes
b/c the error is correctly handled, not b/c it was silently swallowed."

Yes, but I think that this is exactly what pilif is objecting to: the fact
that there is no way for the callee to _force_ the caller to deal with the
error condition (I'm not sure that this is entirely correct, given that Go has
the panic/recover keywords that are later discussed in the article, but then
you might as well use exceptions).

IMO, this is very similar to what happens with use-after-free and other types
of memory reference errors: they are _extremely_ hard to debug because the
symptoms/results of the error can be totally disconnected from the original
source of the error, in some cases by minutes or longer. You simply don't want
to have a situation where improper state is allowed to linger and further
pollute/corrupt any subsequent computations.

~~~
zzzcpan
And yet forcing the caller to deal with errors still doesn't prevent improper
state in any way.

~~~
TimJYoung
It does if they don't handle the exception, which is the situation being
described.

~~~
zzzcpan
Errors cause side effects, which is how you get to improper state. How does
forcing the caller to handle errors helps with removing side effects?

~~~
TimJYoung
Because the caller either deals with the error (and fixes the state) or they
don't, but there's never a case where the callee was unable to _force_ the
caller to fix the state. If the caller then goes on to just handle/suppress
the exception and continue with the invalid state, then at least they're
making an explicit decision that is on them.

~~~
zzzcpan
That's the point, forcing to handle errors doesn't force to do it properly.

~~~
msbarnett
It's still strictly better than just letting people not bother to handle them,
which is never proper.

~~~
dnautics
Is that true? What if you have an error that happens once every 10^9 requests
and the service failure isn't critical (no one dies). Isn't it better to just
not bother with the error, let the service keep running and don't worry about
it?

~~~
pilif
That depends on whether you are going to be the person who has to find out why
corrupted data has been written, when it was corrupted, what the original
value was or even worse, whether a given record is corrupted or not because
the corrupted ones look exactly the same as some value of non-corrupted ones.

As someone who has been in that boat, I can tell you that request termination
due to an uncaught exception is infinity times better than the horror of
debugging data corruption, even (or especially) when it only happens ever 10^9
requests.

~~~
dnautics
If it's 10^9 requests, then obviously they are low-cost processes in the first
place, so you're better off just re-doing the job that failed.

~~~
pilif
That's what I mean. Yes.

But when the caller missed checking some error code, then nothing won't notice
it has failed and now you have corrupt data to deal with. _That 's_ why I
think blowing up at the time when something goes wrong is so valuable.

------
a-saleh
Man! I actually like half of Golang's error handling practice. Returning
errors and dealing with them in a simple case sounds nice, there is no magic
required, no need to think "am I forgetting to catch an exception here?"

On the other hand, the fact that we are returning a tuple of (result, err)
where by convention err should be nil if everything worked screams as a
terrible design to me.

But I might be influenced by reading too much Haskell/ML inspired languages :D

Once you understand applicative validation [1] or exhaustive pattern matching
on row-polymorphic sum types [2], other error handling methods just seem so
crude :)

[1] [https://leanpub.com/purescript/read#leanpub-auto-
applicative...](https://leanpub.com/purescript/read#leanpub-auto-applicative-
validation) [2] [http://keleshev.com/composable-error-handling-in-
ocaml](http://keleshev.com/composable-error-handling-in-ocaml)

~~~
jstimpfle
Who said (result, err) should be a sum type? Why must the result be invalid
when err != nil?

~~~
dwohnitmok
It doesn't. You can use an inclusive sum type (i.e. inclusive or), which would
be success, error, or result with accompanying error. You can think of it as
success, fatal error, or non-fatal error.

The same machinery still works and it's pretty straightforward to convert
between the two representations (so you can track which parts of your code
bail out at the first error and which parts try to soldier on).

See for example
[http://hackage.haskell.org/package/these](http://hackage.haskell.org/package/these)
or
[https://typelevel.org/cats/datatypes/ior.html](https://typelevel.org/cats/datatypes/ior.html)
or [https://github.com/purescript-contrib/purescript-
these](https://github.com/purescript-contrib/purescript-these)

For a lot of FP languages that take this approach to errors you'll have three
representations (bail out at first error, continue on nonfatal errors, and
bail on any error but first try to accumulate as many errors as possible as a
batch) that all have conversions between them depending on what you want the
semantics of that section of your code to be.

~~~
merb
btw haskell also has a concept of exception, so it's not like that you need
These or Either for all cases, in some cases using a exception might be good
enough.

~~~
dwohnitmok
Exceptions do exist but their usage in Haskell is mainly restricted to code
living in IO (as a result of both community customs and the restriction of
handlers to IO code).

FWIW personally I think of exceptions in Haskell almost exclusively as a tool
for thread management and tend to shy away from using it for only error
handling (although the two do have some overlap). But that's a more divisive
topic in the Haskell community at large than the ban on exceptions in pure
code.

------
markbnj
>> Relying on documentation is not a silver bullet either: poor documentation
is still very much a thing.

>> It makes sense always to check the documentation to find out whether an
error type is a part of the function signature.

O_o. So on the one hand you don't know if a function will throw an exception
and can't rely on the docs, while on the other you don't know if it will
return an error code so you should check the docs.

I don't write Go yet, although I look at a fair bit of it. A friend of mine
who works a lot in it tells me he was put off at first but basically decided
to trust the language's idioms for now. Until I have more personal experience
my outlook is: "man, I think I would really miss exception handling" with a
smattering of "boy that sure looks more error prone to me."

------
NateDad
Gah, my eyes! Why is the font so huge? And why doesn't zoom change it? I have
to like step 5' back from my screen to read it....

Otherwise...

I have been writing Go for about 6 years... errors make so much sense to me.
Why have some external codepath for "file not found"? Why is that different
than "name == bob"? They're just data that is in one state or another. You
check for them the same way, with an if statement. There's nothing magical.

~~~
erik_seaberg
If a function can't succeed there's no reason to continue executing it.
Stopping and reporting the error to your caller is the common case and should
not be discouraged by adding friction and obscuring the useful parts.

~~~
weberc2
And yet no one is deterred from proper error handling in Go. On the other
hand, I see people fail to properly handle errors all the time in our
Python/JS shop.

EDIT: This isn't some language partisanship; it's my observations from years
of experience with the aforementioned languages (I work in a Python/JS shop).
I'm guessing there are no formal data about this, so I'd be curious to hear
other anecdotes whether they agree or conflict with mine.

~~~
smbullet
Yeah, I'm not sure why you're being downvoted. We also use Python at work and
reviewing code is an absolute nightmare because you end up needing to dive
deep into each function call to see if there are any exceptions being ignored.
We're trying to enforce adding all possible exceptions in the docstrings but
it's difficult because third party libraries (and even the standard library!)
sometimes don't document them. Typically we have a catch-all Exception in our
main loop and recover from there in case there's something we missed in dev
and staging. I'd much prefer being able to know which functions have the
ability to fail and why.

------
recursive
I think fancy custom ligatures are actively un-helpful when trying to
demonstrate something about code to a wide audience. Is it a custom operator?
Is it a fancy codepoint like APL or Julia? Is it a pre-processor macro?

Personally, I don't like them in my editor either, but if you want to use one
that's fine. But they're pretty confusing when encountering in code online.

~~~
Matthias247
100% ack. If I wouldn't have known ligatures, I would be totally confused
about the syntax in this code examples. Am I expected to type this character?
Where is this thing on my keyboard, etc.

------
peterashford
Looking at that pile of vomit code and claiming to have reached some kind of
nirvana is just ridiculous. That article is an argument against go's error
handling if I ever saw one. One shouldn't have to go to that much effort just
to make the handling of errors in Go, sane. To come up with that monstrosity
one one hand while calling try ... catch "verbose" on the other hand is
hypocrisy of the highest order.

------
runn1ng
Even go authors themselves know the current error handling is not ideal,
that's why they want to change it in go 2...

see

[https://go.googlesource.com/proposal/+/master/design/go2draf...](https://go.googlesource.com/proposal/+/master/design/go2draft-
error-handling-overview.md)

~~~
shaklee3
He talks about this in the article.

------
jayd16
So how is this better, cleaner syntax than checked exceptions in Java, for
example? I thought checked exceptions were considered a failed experiment. The
convention in go is to always handle the errors so isn't that the same thing?

Is it the lack of forced stacktrace that makes flow control through exception
more palatable?

Do people just like convention over compiler enforced correctness?

Or is my understanding incorrect and devs don't really like go's error
handling?

~~~
erik_seaberg
There's a difference between "errors should alter the flow of control" and
"error types should be transitively declared". The latter failed in Java
because the language didn't have enough support for handling the layering
violation. E.g., if the X ctor throws suddenly you can't implement Supplier<X>
without dirty hacks like writing an explicit handler that does nothing but
rethrow an unchecked type.

~~~
jayd16
Throwing in constructors is an anti-pattern imo but assuming you need it, how
does go solve this? Would you not return nil and an error that you would
immediately check? Seems like defensively checking return values in go isn't
that much better than catching every exception.

Maybe I don't understand the correct go implementation.

------
makecheck
It’s actually kind of amazing that programming languages basically make it as
hard as possible to figure out what’s going on when you have the temerity to
check for errors, and it is easier to read code that _doesn’t_ care.
Programming books love to exclude error handling “for brevity”, when the real
problem is that the mechanisms for doing so are insane to begin with.

Conditionals are frustrating because they tend to be paired with indentation,
which doesn’t "diff" well in a lot of tools and makes simple changes seem more
extensive than they really are. In a sense, I shouldn’t have to re-indent half
a function just because I happen to be changing the “preferred/happy path”
that is buried inside several error checks.

On the other hand, early returns are frustrating because they enable lazy
programmers to make quick hacks at the expense of complicating later
maintenance (e.g. instead of having one “normal” path to consider, the next
programmer has to find and understand all the short-cuts that someone has
introduced throughout the code and make sure they will all work).

I suppose the most “compatible” change to existing languages would be
something like a keyword or syntax to identify code that is only handling
errors. That way, at least IDEs/editors/etc. could offer ways to intelligently
hide code that apparently isn’t part of the normal flow of a function.

Fortunately languages are now more likely to have good mechanisms for
declaring local blocks of reusable code (not banished to functions on far-away
lines) so it is now more practical to write code in logical chunks that aren’t
quite as indented. You can write a sequence of operations, maintain it without
ugly "diffs", and still have indentation and error-checking, etc. at point of
use.

------
kccqzy
The combination of panic and recover is exactly like raise/throw and catch.
One could even reasonably argue that both are both just a single specialized
call/cc (You pass call/cc a function to execute, which receives an argument
that allows immediate exit to the enclosing scope.) It's basically a kind of
control flow operator. So what's the novelty here?

------
rauhl
For some time now, I’ve been thinking that it’d be funny to add a Lisp-style
condition system to Go by abusing panic, defer & recover. I _think_ that it’d
be possible (since Lisp’s conditions are really just built atop THROW &
CATCH).

I don’t really think it’s a good idea: Go’s lack of macros imply rather a lot
of anonymous functions to get it to work, which would imply loads of
parentheses & curly brackets. Not to mention that the lack of first-class type
literals would make actually using it close to insane. But it’d certainly be
interesting. It’d _definitely_ not be idiomatic Go. Still … interesting.

~~~
masklinn
> For some time now, I’ve been thinking that it’d be funny to add a Lisp-style
> condition system to Go by abusing panic, defer & recover. I think that it’d
> be possible (since Lisp’s conditions are really just built atop THROW &
> CATCH).

How would you resume execution at the panic point?

A conditions system requires that you don't unwind, afaik Go's panic do in
fact unwind.

~~~
rauhl
> How would you resume execution at the panic point?

You invert it: rather than panicking, then continuing from the panic point if
things recover, you proceed, only panicking to perform a transfer of control.
Interestingly, this is what Lisp does: SIGNAL[0] (the most primitive function)
just looks for applicable handlers and calls them from most- to least-recent;
this means that if a handler transfers control (panics, in Go terms) then the
condition has been handled; if not, SIGNAL just returns NIL; ERROR[1] does the
same thing, but calls INVOKE-DEBUGGER if no handler transfers control;
CERROR[2] is like ERROR, but it adds a CONTINUE restart, which just lets
control resume.

It’s possible to implement this same structure in Go: conditions.Signal()
would search for applicable handlers and execute them in order; if control
transfers then it’d never return; conditions.Error() would invoke one or more
handlers, or the debugger, and would never return;
conditions.ContinuableError() would invoke one or more handlers, or the
debugger, but might return.

The key is that it’s possible to execute a restart in the dynamic context of
the signalling function (via use of Go’s context.Context), and only rewind the
stack when you don’t need to go back down it. I think — I’ve not yet
implemented it!

0:
[http://www.lispworks.com/documentation/lw71/CLHS/Body/f_sign...](http://www.lispworks.com/documentation/lw71/CLHS/Body/f_signal.htm)

1:
[http://www.lispworks.com/documentation/lw71/CLHS/Body/f_erro...](http://www.lispworks.com/documentation/lw71/CLHS/Body/f_error.htm)

2:
[http://www.lispworks.com/documentation/lw71/CLHS/Body/f_cerr...](http://www.lispworks.com/documentation/lw71/CLHS/Body/f_cerror.htm)

~~~
masklinn
I see, you'd only be using panics for the unwinding handler, not for the
actual conditions system.

------
lobo_tuerto
Very annoying and frustrating that you cannot zoom in/out to adjust the font
size in this @%$#!& site. Why take that freedom from your users?

I see a waaaay big font on my monitor (3440x1440).

------
H1Supreme
What's up with the "equal sign with a slash" being used in place of the
literal "!="? I realize it's reference, but why abstract the code away like
that? Seems unnecessary, and potentially confusing for new programmers.

~~~
a9entroy
[https://github.com/tonsky/FiraCode](https://github.com/tonsky/FiraCode)

------
Insanity
When learning Go, I actually liked how it dealt with errors. I like that you
need to aknowledge that an error could happen, but I also like that it's a
normal return value.

It's not valid for _everything_ so I don't agree with it entirely. But, some
things that are exceptions in Java would make more sense as just returns
because it is _valid_ output for a function.

I think it's the distinction between "the method can't perform what it's
supposed to perform due to the input it received" vs "something unexpected
happened and that's why the method couldn't perform what it was supposed to".

I'll try to give an example:

Imagine you have a method in Java for parsing a credit card, which follows a
certain pattern for verifying CSV. If the pattern wasn't satisfied, you could
have a:

    
    
          Throw new IllegalArgumentException("Input does not match CSV string ([0-9])")
    

But to me _exception_ would signify something unexpected happened. Whereas
here I'd like if I could just 'return' that the input was not correct, and
therefore the method could not check the CSV.

Whereas, for example, if you have a method that writes to a File and the file
you provided does not have the right priviledge -> this is an Exception
because the user could not expect this.

I'm having a hard time wording this as consisely as possible, I'm sorry if it
seems a bit incomprehensive but I'll try to TL;DR:

Exceptions: Unexpected behaviour from system Error returns: Wrong input from
user

If that makes sense :)

~~~
masklinn
> If that makes sense :)

You're just trying to justify/rationalise that you like go's arbitrary
positioning on the result/fault continuum (well not continuum, it's not really
1-dimensional), there are few positions which can't be justified there.

> But to me _exception_ would signify something unexpected happened.

Being provided random garbage when prompted for a CVV can reasonably be
unexpected.

> Whereas, for example, if you have a method that writes to a File and the
> file you provided does not have the right priviledge -> this is an Exception
> because the user could not expect this.

Why would the user not expect ACL issues on something well known for involving
ACLs?

------
purple_ducks
Isn't it weird that there's no language which nailed error handling and
patterns 100% and which people can point to confidently and say "Do what they
did - it's bulletproof"

~~~
zzzcpan
That language is called Erlang, people always point it out, nobody listens
though.

------
leshow
There are other ways to handle errors besides Go's overly pedantic way and
exceptions. Either/Result types are in a few languages and are wonderful.

------
AdeptusAquinas
Reminds me of result types and ROP in functional languages, especially F#.
Except in F# callers must handle all conditions. Maybe they can add that check
to golint.

------
tomohawk
I would not ever even think about reusing any package that used the
panic/recover mechanism for error handling. It's that bad.

------
pkulak
Go's error handling is exactly the equivalent of only having checked
exceptions. I've gone the other way, personally, the last few years: I prefer
languages that only have unchecked exceptions.

~~~
erik_seaberg
If you call methods that throw IOException, you can just declare IOException,
you don't have to litter every statement of every method with catch clauses
that hurt readability.

 _Edit:_ where this breaks down in Java is trying to implement interfaces that
_don 't_ declare the exceptions you're then forced to smuggle out.

------
luord
The article is useful, detailing different approaches to... deal with the
ugliness; but it still lost me because of the ridiculous implication that Go's
error handling is in any way right or even _better_ than the try catch flow.
It isn't, it so much isn't that even its designers are tacitly considering
they're wrong with check and handle (Go's own try catch, which the OP himself
mentioned).

Go is not perfect, no language is, and one of its biggest flaws is its odious
error handling. And that is good, because nothing can be perfect.

Apropos of nothing, it's pretty amusing that the go faq entry criticizing
exceptions has a typo.

