
Exploring Error Handling Patterns in Go - trptcolin
https://8thlight.com/blog/kyle-krull/2018/08/13/exploring-error-handling-patterns-in-go.html
======
dullgiulio
A cursory look at the article, shows that the most important observation about
error handling in Go is missing.

Errors should be "decorated" (wrapped, contextualized...) in 99% of the cases.

In the end you get errors that describe step by step what your program tried
to do and why it failed, for example:

* could not load profile: could not open file: permission denied.

* could not download profile image: could not open URL: HTTP GET failed: network is down.

This has many advantages:

1\. Much more readable than stack traces (especially if they include source
file and line information or exception class names: users don't care about
those.)

2\. Errors are still easy to grep in code to work out the program flow (the
stack trace, basically.)

3\. When reading the code, you can see from the error context strings what the
code is actually doing. Basically it serves a function of comments and (unlike
comments) error strings remain up to date.

It is definitely verbose, especially the not equal nil part, as it's a result
of Go attempt not to have special cases. Also it's a pity that errors can be
silently ignored: maybe Go2 could be stricter here.

Overall, I think this is one of the best approaches at error handling.

~~~
Groxx
In my experience, this just becomes arbitrarily close to "re-implement your
stack trace by hand with space-delimited words instead of
camelCaseFunctionNamesOrWhatever".

I'll _overwhelmingly_ prefer an always-correct stacktrace over a hand-
recreated one that sometimes collapses multiple branches into a single
ambiguous on. At least then the devs can help me when it fails. And stack
traces and concatenated strings are in no way appropriate error responses for
humans unless you're expecting them to be able to navigate the source code, so
neither does anything for the "provide a helpful error message for non-
programmers" problem.

\---

this is why stuff like
[https://github.com/pkg/errors](https://github.com/pkg/errors) exists. wrap at
the deepest level / where the error originates, and it's relatively rare that
you need to add context at higher levels. If you want user-friendly errors,
you need something dramatically more sophisticated.

~~~
jy3
Just using WithStack() from "github.com/pkg/errors" on any error that
originates from outside my repository has been my go-to rule for any Go
project. It has never disappointed.

~~~
jashmatthews
Like an exception?

~~~
jy3
Except it's just a returned value and not a goto lookalike.

~~~
dragonwriter
Exceptions—which go straight up the call stack, like returns—are nothing like
gotos.

~~~
trisiak
The similarity in both needing to have stack trace does not make them very
alike yet. They serve similar purpose and for that purpose the stack trace
serves value to the developer.

Returns go straight up the stack only if you choose them to. Caller doesn't
have to propagate errors and return immediately, it can hold onto them and/or
process them in the natural place they occur, they are just values. The
returns from your function can be found with "grep return".

Exceptions, by default, break the natural control flow unless you wrap
everything with try/catch. Even then, a lot of constructs won't be very
natural and you really will have to get out of your way to identify the source
of the exception, which very frequently is more important than it's type.

~~~
jashmatthews
Why would finding the source of the exception be difficult? The stack trace in
exception-based languages goes back to the actual line, as opposed to a stack
trace from an error-as-value based language, where it only goes to where you
trigger generating the stack trace.

~~~
trisiak
I meant finding it in the context of a coder who writes a function. It's hard
to identify which expression and statements can cause exceptions and
effectively short-circuit your function. In contrast to explicit errors-
returned-as-values.

(Panics of course can cause similar thing in Go but that's the reason why they
should rarely be recovered and not used as a value propagation mechanism.)

~~~
Groxx
Is that any worse than the ambiguity in e.g. code like this?

    
    
        func thing(arg) {
          otherThing(arg) // does this return an error you're ignoring?
        }
    

It even has a similar problem, where void returns -> err returns on code
changes are not visibly discoverable (similar to a newly-throwing func).

Granted, you could turn this into a compiler error. But it's not currently.

\---

Anyway. Given that so many not-prevented-by-the-typesystem operations panic, I
don't think it's reasonable to assume that _any_ func call will not panic,
especially not in the future since they may change. So you _already_ have to
program with `defer` to maintain your invariants, which is exactly the same as
with exceptions, except you now have to deal with both possibilities for
nearly every func.

------
tln
I appreciated the article, but the error handling in Go just bugs me. So
verbose...

The built-in tool does not even warn about unused errors... not `go build`,
and not `go vet`. What's more important, an ignored error or an unused import?

[https://play.golang.org/p/j-oXsZz51ki](https://play.golang.org/p/j-oXsZz51ki)

~~~
pstuart
That's by design -- programmer's choice.

~~~
TheDong
I don't understand.

Go errors out on unused imports, but you can type "import _ foo.com/unused-
import" to not error out.

Why doesn't 'errors.New("asdf")' error out and require you to instead write '_
= errors.New("asdf")' to ignore the result

I think the real answer is not that it's intentional design, but rather that
the original compiler was not powerful enough to implement that feature
easily... and once go hit 1., it was impossible for them to add new warnings
or errors because there are no warnings and errors are backwards incompatible.

Sure, that means developers use third-party tools for warnings because the go
compiler refuses to ever have warnings (that compromises the pure beauty of
the language obviously), but at least that means it's only the users that have
to deal with the complexity of using more tools, the compiler developers can
ignore it.

~~~
mopsy
I don't think it's because of the complexity. I'm quite sure it would not have
been difficult to do.

One of the reason is that you don't always want to check the error. The most
common one is fmt.Println.

I would not like to always write

_, _ = fmt.Println("Hello, playground")

~~~
stubish
You certainly do not always want to write _,_ = fmt.Println("Hello,
playground"), and I think this points out the real lack. What do you want your
program to do if fmt.Println starts failing? I think, unless you are
explicitly checking for errors, that in all other cases you want it to crash.
Rather than silently continue. Which is a bug, and a potentially disastrous
one, that is endemic in Go code (and, to be fair, plenty of other languages).
Thankfully the practical risk of this particular case is tiny.

This is why you want unchecked errors to implicitly bubble up. Which is what
exceptions give you, or perhaps a syntax with implicit error return values
rather than Go's by-convention approach.

~~~
pstuart
If fmt.Println fails you've likely got bigger problems and the game is over.

And if it means a lot in this case, wrap the function and use it instead.

------
divan
I wish more developers could do "investigation" like this for a new languages
they learn.

For me, the main difference between Go's way of handling language and the rest
of mainstream languages is that it makes error handling __unmagical __.

It literally says – errors are just like any other return values. Let's say,
if you have function `sqrt` and return a value, and then call this function –
you probably is interested in this return value and should handle it somehow
(or mute with `_`). Now, the same applies for errors - if function returns the
error, you likely to think how to handle it – do something in place or
propagate up the stack.

There is also a cultural moment to this. As we mostly learn by examples, and
most Go code has proper error checks (not equal to "proper error handling" but
nevertheless), it makes newcomers to do the same as well, even while
disagreeing with Go's way. I've heard from many devs that Go was the reason
that made them appreciate proper error handling.

And honestly, I feel this too, and I think the reason is that in Go it's too
easy to "handle errors properly". I never had this feeling with languages with
exceptions, where I had to read whole books (!) just to learn how to properly
use them and be confident in the way how I handle errors. (this is just an
example, not the spark to start return values vs exceptions battle, just in
case)

~~~
erik_seaberg
The flipside of easy-to-learn is there's no payoff for getting better with the
language. Your code will always be exactly as tedious as novices' code because
they'd rather conserve compiler cycles than spend them to amplify programmers'
work.

~~~
pebers
On the bright side, your code will always be as easy to understand as a
novice's code too, so it conserves other programmer's mental cycles as well.

~~~
marcosdumay
> your code will always be as easy to understand as a novice's code too

Yeah, that seems about perfectly correct. Have you looked at novice's code? Is
it easy to understand?

~~~
pebers
Yes, we have adopted Go for a lot of things at work and I have reviewed code
of many of our devs who are more or less Go novices. It has indeed been pretty
easy to understand - much easier than other languages (notably Java in this
regard, but Python falls afoul of it a bit too) where people tend to write
things in quite different styles and/or with excessive abstraction that made
it much harder to understand what was going on.

------
macrael
What is not covered here, and what I'm still searching for a good pattern for,
is being able to return different errors depending on the type of failure.

Suppose you have a function that fetches a model from your database. It can
return an error if the given user doesn't have permission to fetch this model,
or it can return an error if your db connection barfs for some reason. The
calling function needs to be able to differentiate between the two errors.
Most of what I've read on the subject makes it seem like people prefer to only
ever check if err != nil.

The two options I've seen in the wild are:

1\. Create a constant for a given error, like:

    
    
      var ErrFetchForbidden = errors.New("FETCH_FORBIDDEN")
    

Then the calling function can do:

    
    
      if err == ErrFetchForbidden {
        return 403
      } else if err == ErrFetchNotFound {
        return 404
      } else {
        return 500
      }
    

2\. Create a custom type for your error like so:

    
    
      type ErrFetchForbidden string
    

this has the benefit that the errorer can put more specific info into the
error besides the Error() string.

    
    
      var err ErrFetchForbidden = "error retrieving the user object"
      return err
    

and then the caller can switch on type

    
    
      switch v := err.(type) {
        case ErrFetchForbidden:
          return 403
        case ErrFetchNotFound:
          return 404
        default:
          return 500
      }
    

We've gone with option 2 for now, (wrapping them with the pkg/errors package)
because it seems simpler. Anyone else have good patterns for handling this?

~~~
cube2222
There's another one I often use:

Create a custom error type, for example DB Error:

    
    
      type DBError struct {
         Temporary bool
         NetworkBased bool
         Cause error
      }
    
    

Now you can provide functions like IsTemporary(err).

Otherwise, you can use 2# with a twist, instead of matching on a type, you can
do:

    
    
      switch {
         case isErrFetchForbidden(err):
         case isErrFetchNotFound(err):
      }
    

or even:

    
    
      IsBadRequest(err)
      IsInternal(err)
      IsTimeout(err)

~~~
macrael
So you then define your function to return the type DBError instead of a
generic err type. That makes sense to me but for some reason some of the stuff
I've suggests that just returning err is more go-like.

~~~
cube2222
No, you don't, DBError should implement the error interface.

IsTemporary also takes an error and does something like:

    
    
      if err, ok := errors.Cause(err).(*DBError) {
        return err.temporary
      } else {
        return false
      }

~~~
macrael
oooh!

------
mdwhatcott
> The other bit of good news is that you can't unknowingly ignore a returned
> error, like you can with an unchecked exception. The compiler will force you
> at a minimum to declare the error as _, and tools like errcheck do a good
> job of keeping you honest.

Actually, we unknowingly ignore returned errors much more often than we think,
like when we call a function and opt out of assigning any of the return values
to variables. Consider this function, which returns a single value (being an
error).

    
    
        func Failure() error {...}
    

You can always choose to call an error-returning function without declaring
any placeholder (`_`):

    
    
        Failure()
    

There are several commonly used functions that return errors that are
regularly ignored. How about `io.Writer`?

    
    
        writer.Write([]byte("Hello")) // returns (n int, err error)
    

It's quite common to call that function without feeling a need to check on the
bytes written or a possible error. Or, consider whether you consistently check
the return values of `fmt.Println()`, which also returns `(n int, err
error)`...

~~~
ecnahc515
errcheck is a good tool to help with this.

------
arendtio
For everybody who is interested in improving his error handling skills:

[https://dave.cheney.net/2016/04/27/dont-just-check-errors-
ha...](https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-
gracefully)

------
rco8786
if err != nil return err

if err != nil return err

if err != nil return err

[https://github.com/docker/cli/search?q=%22if+err+%21%3D+nil%...](https://github.com/docker/cli/search?q=%22if+err+%21%3D+nil%22&unscoped_q=%22if+err+%21%3D+nil%22)

[https://github.com/kubernetes/kubernetes/search?q=%22if+err+...](https://github.com/kubernetes/kubernetes/search?q=%22if+err+%21%3D+nil%22&unscoped_q=%22if+err+%21%3D+nil%22)

[https://github.com/coreos/etcd/search?q=%22return+err%22&uns...](https://github.com/coreos/etcd/search?q=%22return+err%22&unscoped_q=%22return+err%22)

[https://github.com/influxdata/influxdb/search?q=%22if+err+%2...](https://github.com/influxdata/influxdb/search?q=%22if+err+%21%3D+nil%22&unscoped_q=%22if+err+%21%3D+nil%22)

The reality of Go's error handling is that you just implement exactly what
exception bubbling does painfully by hand.

~~~
chmike
This is not correct. Exceptions do different things than report an error. They
unwind the stack. That's why they are called exceptions and not errors.

One important benefit of Go's error handling pattern is readability. With
exceptions, it's not easy to see who handles it and where. There is indeed
less code, and that's nice for the writer, but from the reader perspective,
error handling becomes obscure. And from the quality control point if view,
this becomes unsafe.

~~~
h1d
At the cost of making the entire logic's readability less which to me is more
important than sometimes getting confused where errors bubble up to.

The philosophy is different when, for example the author of Ruby wanted to
make coding fun for programmers and does a good job at it and Go is sticking
to 'this must be right' approach and breaks some people's heart.

Personally I'd appreciate being more 'fun'.

~~~
DJHenk
Depends on your definition of fun, I guess. I personally don't like Ruby
because of that fun-factor. In most cases, it makes programming easier for the
novice, but more complicated for the experienced.

This is because to make it easier for the novice, there are all kinds of
constructs that try to make the code imitate normal English. But coding
software is a completely different thing than writing text, thus the English-
like front is in fact a smoke screen that hides the real gears.

A small example would be the unless keyword. It completely throws me of each
time I come across it, because it reverses the order of evaluation:

Do something, unless condition applies.

I read that from left to right, so in my mind "Do something" has already
executed, but then I have to go back, because the condition might not apply.
This get really 'fun' if the condition is something negative.

I like Go just because it way more simple. Even it is a bit more verbose in
the error handling, everywhere else it is very minimal and clear.

~~~
ethelward
That's only the postfix version of unless though, and you could use if the
same way. And inversely, you can use unless is its own block, like if.

------
Sidnicious
The first time I used Go on a big project, error handling bugged me enough
that I wrote a package that let me use errors like exceptions:

[https://github.com/s4y/go-exc](https://github.com/s4y/go-exc)

I’m not sure if I’d use it again today, but it was a fun exercise.

