
Error Handling in Go (2016) - lelf
https://www.innoq.com/en/blog/golang-errors-monads/
======
userbinator
Both this and Rob's article seem to be hiding an inefficiency: if one of the
early functions in the chain (hint) fails, the rest of them are still called,
just to do nothing. It also hinders understandability and debugging, since
you're continually bombarded with "if(err)" after the first failure.

I've thought about this problem too (error handling in general), and with that
hint of this being a "chain", arrived at this solution which I think is
extremely concise and elegant:

    
    
        if((err = func1()) ||
           (err = func2()) ||
           (err = func3()) ||
           (err = func4()) ||
           ...
        ) {
         /* handle the error */
        }
    

or if you want to get more fancy and know which step failed,

    
    
        int step = 0;
        if((step++, err = func1()) ||
           (step++, err = func2()) ||
           (step++, err = func3()) ||
           (step++, err = func4())
        ) {
        ...
    

Of course this can be modified suitably if your error vs. success values are
different, but the structure is the same: a chain of actions one after
another, broken only by short-circuit evaluation. It's surprising that this
pattern isn't seen more, because I've shown this to a few others and the
response has almost always been an initial puzzlement followed by "wow, that's
extremely neat" or "why didn't I think of that?" This is certainly an example
of "use the language".

~~~
ben_jones
This is cool but I think its brittle against real world scenarios:

    
    
        func (r *Resource) get(w http.ResponseWriter, r *http.Request){
            user, err := r.authenticate(req)
            if err != nil {
                r.WriteJSONErr(w, err)
                return 
            }
    
            var records []*User
            if err := r.db.Select("select id, name, created_date from users", &records); err != nil {
                r.WriteJSONErr(w, err)
                return
            }
    
            r.WriteJSON(w, records)
        }
    

You would have to reformat all your function signatures to receive maybe a
pointer to populate and return only an error, which could work but meh.

~~~
johan_larson
Yes, the pattern isn't useful for function calls that return results that are
used as parameters in later function calls, which tends to happen all the
time.

~~~
userbinator
It works as long as the return value can be used to distinguish failure from
success:

    
    
        (f = fopen(filename, "rb")) &&
        ...
        (ptr = malloc(size)) &&
        fread(ptr, 1, size, f) == size &&
        ...

------
natefinch
I've been writing Go for over 6 years. Error handling at the source of failure
is a feature, not a bug.

Rob Pike's example code is bad and I would not allow it through a code review.
Why? It makes the code lie. It appears as though the writes cannot fail, but
in fact they can, and later writes become no-ops. On top of that, it means
that you lose the context of _which_ write failed. Did you fail writing the
header, the body, or the footer? You can't know anymore.

This is like

    
    
       try {
            doThing()
            doThing2()
            doThing3()
       catch (Exception ex) {
          // handle
       }
    

You can't tell what failed. You can't tell what is _possible_ to fail. The
handler can't differentiate.

~~~
syrrim
Code lies all the time. This is called an abstraction, and is very useful. The
abstraction that rob's code provides is that it makes you think of writing not
as doing something with the OS, but as merely pushing to a buffer, with flush
being the call that actually commits the transaction. Therefore, flush is the
call that can fail. Is this literally what is happening? No, but it is a much
easier way to think of things, and makes the code more readable.

>Did you fail writing the header, the body, or the footer? You can't know
anymore.

You are writing to the same stream in both cases. The error wouldn't be caused
by the header or footer data, but by some property of the stream, like it
being closed, or the disk running out of space. Knowing whether the failure
occurred while you were writing to the header or footer is unnecessary
information, and therefore obfuscatory.

~~~
ereyes01
I disagree with this rationale, but I'll let someone smarter than me make the
argument:

"The purpose of abstraction is not to be vague, but to create a new semantic
level in which one can be absolutely precise." \- Edsger Djikstra

Or said another way, a good abstraction automates a precise set of tasks
instead of adding ambiguity.

~~~
firethief
I'm not sure that's the argument you want to use it as. Dijkstra is surely not
arguing that abstraction should not lose information (that would actually not
be abstraction at all); he is saying that the important thing about it is that
it "finds" the pertinent information (i.e. by throwing away the irrelevant
details). He's not redefining the term as something incompatible with its
usual meaning, but proposing a perspective, taking the information loss as a
given.

~~~
jstimpfle
I think the name "abstraction" is unfortunate in Dijkstra's sense. The notion
of abstraction he's talking about is not about the process of going from less
to more abstract. The abstraction _is_ the more abstract. In fact, ignoring
the path by which we arrived at the abstraction, it has absolutely no
intrinsic relation to the original, more real, less abstract model.

Maybe we're just trying to say the same thing here.

------
bdamm
It's totally on point. I've been writing a fair bit of Go lately and
consequently am firmly in the Generics! Now! camp. The pace of writing in this
language is almost snail like, and a lot of that is due to re-implementing
common idioms like popping and pushing from/to a stack and then debugging the
little typos that creep in around the edges. It's freaking annoying, and it's
a shame because the language is otherwise nice to work with. If it weren't for
the testing workflow I'd have given up on Go entirely, just because of how
annoying it is to re-implement the basics, over, and over, and over...

~~~
johan_larson
I don't know. I've worked in three different languages for years on end, and
they all have different pain points. C++ is too complicated, and generics are
part of that complexity. Java has an ok basic language, but the ecosystem is
weird and fad-prone, jumping from beans to patterns to annotations to
injection, searching for the One True Way. Go is a little too simple, which
sometimes leads to rewriting fairly straightforward stuff that a few clever
general mechanisms would let us write once.

This experience makes me suspect that there will always be some pain point
with the language. We'll never be happy; that's impossible. The only thing we
can do is choose what type of unhappiness we are willing to live with.

The thing I love about Go is its fundamental clarity. It's very upfront and
literal. I find it easy to understand what is happening in any particular bit
of code. And I suspect that whatever complexities we add would compromise this
clarity in the name of brevity. I'd spend less time being bored, and more time
being puzzled or incredulous. And fundamentally, I'd rather be bored.

~~~
snarfy
Have you tried C#?

~~~
NateDad
C# has the feature-bloat of C++, except with a garbage collector. (I wrote C#
for 9 years and even when writing it 40 hours a week, I still had trouble
keeping up with all the features that continually came out)

~~~
tonyedgecombe
C# has become quite a big language although it manages to hide the complexity
quite well compared to C++. There is very little in the language that will
trip you up, unlike C++.

The only problem I have with it is it is still really Windows only, hopefully
that will improve with .Net Core 3.

~~~
tluyben2
I have been writing stable and robust C# software on Linux and Mac OS X for
many years now, so not sure what you mean. Starting with Mono and now solely
.NET Core 2 & 3\. I know many people who do that as well. And our deployments
(all our prod/test/staging servers) went from Windows-only 15 years ago to
Linux-only since 4 years.

Maybe you are talking WPF / Desktop only; there are other options for Mono but
yes, there you would be right. However the trend is, unfortunately, toward
browser interfaces (Electron etc) and those you can do on Linux/Mac already
with .NET Core.

~~~
tonyedgecombe
My experience with .Net Core 1 and 2 was that it felt unfinished. I am
planning to take another look when 3.0 is released because I do really like
C#.

I wasn't thinking of Desktop although I would love to see something strong
emerge there (other than Avalon).

~~~
tluyben2
> My experience with .Net Core 1 and 2 was that it felt unfinished

Ah, curious what made you feel that. We ported massive code bases of ASP.NET
and commandline tooling over to .NET Core since the 1 and had not many issues.
It's a much better experience now but it never felt unfinished to me.

I have to admit that I have been writing software for a long time and one of
the things I automatically do is abstract (not too far, just far enough) the
underlying implementation of whatever I make/made. So our old ASP.NET code was
very easy to port for that reason; I never use internals directly and still do
not. For instance MVC looks more or less the same anywhere so I just use plain
old C# classes as controllers so they can be reused by apps, other (non
ASP.NET) frameworks, commandline, tests etc. It adds a thin layer below them
so they work but it saves a lot of time and with the coming of .NET Core it
proved smart once again.

~~~
recursive
In the early days, my impression was that the build tools were totally
overhauled every few months, and not always in compatible ways. JSON projects,
xml projects, dotnet-cli.

------
nprateem
Error handling has to be one of the worst things about go. Even though IDEs
can highlight where you've forgotten to bind an error returned from a function
to a variable they don't prompt you if you then forgot to return that err back
up the call stack - it's easy to just log the error and forget to actually
return it.

The only way I've found to avoid RSI and make error handling just about
tolerable is to use an IDE that supports code autocompletion - e.g. type `err`
and hit tab and it should replace it with an `if err != nil {}` block. The
other thing is to use pkg/errors[1] and wrap the error so you've actually got
a stack trace if you want to log the error further up the call chain.

Error handling is a solved problem - exceptions, or like the article says
monads if the language is functional. The fact that Go supports neither out-
of-the-box is a frustrating waste of time.

[1] [https://github.com/pkg/errors](https://github.com/pkg/errors)

~~~
eadmund
> Error handling is a solved problem

True.

> exceptions

Nope. Exceptions aren't capable enough; you really want conditions & restarts:
[http://gigamonkeys.com/book/beyond-exception-handling-
condit...](http://gigamonkeys.com/book/beyond-exception-handling-conditions-
and-restarts.html)

Conditions can be signalled, handled & resignalled like exceptions, except
that they can be optionally ignored and handlers can optionally choose to
_restart_. An exception unwinds the stack, while a condition is handled prior
to unwinding, which means that the available recovery strategies are more
numerous.

~~~
YorkshireSeason
_" Right tool for the right job"_

Exceptions are an extremely useful and widely used tool for dealing cleanly
with certain classes of errors (through a non-local jump out of a context to a
dynamically bound target), let's call them _simple errors_. Maybe something
that can be oversimplified like this:

    
    
       def main ( ... )
          try
             ...
          catch 
             case e => print "Something's wrong, aborting"
    

(Note that in Go's original main use case (writing low-level servers), such
simple error handling is rarely what's needed.) If you need restarts etc, then
standard exceptions are not the right tool. In my experience, it's worthwhile
to stop thinking about errors in such situations but see the behaviour that
you are tempted to classify erroneous as part of the normal algorithm, using
control constructs other than exceptions.

------
networkimprov
The design pattern espoused here is dated. The emerging consensus for
exception-free Go error handling entails automatic dispatch of errors to local
handlers. See the golang wiki[1].

I've drafted a menu of possible requirements for Go 2 error handling[2]
because the Go team's Draft Design document omitted a requirements section.

[1]
[https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback](https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback)

[2]
[https://gist.github.com/networkimprov/961c9caa2631ad3b95413f...](https://gist.github.com/networkimprov/961c9caa2631ad3b95413f7d44a2c98a)

------
lenticular
This kind of reminds me of the story of promises in javascript, which are
almost but not quite monads. Among other things, this has made implementing
cancellation difficult. In addition, if it were monadic, the async/await
syntax could have been trivially generalized to arbitrary monads. That would
have been nice.

~~~
BreakfastB0b
You can get async / await for arbitrary monads in JavaScript using generator
functions and coroutines.

~~~
michaelsbradley
Also, IxJS and RxJS provide some really nice sugar for working with higher-
level abstractions around "pull" and "push" flows. The protocols associated
with `Symbol.Observable` and `Symbol.asyncIterator` have small surface areas,
so it's not difficult to hand-write implementations (using generators and
async generators) to adapt code that wouldn't otherwise fit in the flow.

------
blondin
little off-topic. am i the only one who thinks Go is becoming a verbose
language?

some of the creators i have followed for years because on their take on
succinctness and simplicity in the software realm. and there we are with Go
becoming somewhat verbose to write in my opinion. as opposed to C or Python,
or even Rust, if you will, for example...

~~~
Groxx
I find the language _mostly_ concise, i.e. new types / funcs / etc are about
as small as they can be (without type inference), but the type system tends to
result in an _absurd_ amount of error-prone duplication and/or wrappers. And
the wrappers that can clean up the duplication take a fair amount of work each
time, and must be repeated each time, so they really only exist when there are
_dozens_ of places where it's useful, rather than some smallish number -
everywhere else you just `if err != nil { return err }` and that just keeps
accumulating.

------
letientai299
Compare the 3 approaches, I rather go with the first one. Yes, it's verbose,
ugly, repetitive. But It's dead simple, easy to understand. The other two
approaches just make the code harder to reasoning once it getting bigger.

~~~
tirumaraiselvan
Except your program won't grow bigger with the "monadic" error handling
approach.

------
vldo
so, what's the resolution, 3 years later?

~~~
Groxx
N years later, maybe we'll have the handle/check proposal[1] implemented.
Which I'm actually fairly optimistic about - much of my error handling would
be simplified by it, since in many cases (90%+?) any error can be treated as
fatal.

There are, fortunately or unfortunately, a lot of very good counter-arguments
and alternate proposals and specific implementation debates. So I'm not sure
who long it'll take, and as much as I Want It Now™ I do think it's important
to be careful for the long-term health of the language.

[1]:
[https://go.googlesource.com/proposal/+/master/design/go2draf...](https://go.googlesource.com/proposal/+/master/design/go2draft-
error-handling-overview.md) , or
[https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback](https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback)
for the current state / counterpoints / etc

------
wahern
FWIW, persistence of I/O errors is precisely how stdio works in C. When fwrite
encounters an error it will continue to return failure (for the same FILE
handle) until invoking clearerr. ferror is used to query the error value.

~~~
Groxx
And I dislike the persistent-error setup in Rob Pike's example for the same
reason as disliking global errs in C: they're easy to forget, _especially_
because they're abnormal. And history keeps showing these things being
forgotten in large numbers, so I don't see why it wouldn't repeat with Go.

------
dilatedmind
In this example could we not just use a for loop

