Golang, how dare you handle my checks!

A Critique of the Go 2 Error Handling Proposal

At last, the Go gods have put error handling on the Go2 roadmap, and released a proposal in two parts: Overview and Draft Design. I peered into them with interest, and was swiftly and severely dismayed. (Indeed, so alarmed that I drafted an alternative.) So what’s wrong?

0.There’s no discussion of requirements. The Overview document has a short Goals section, with just four points: small-footprint error checks, developer-friendly error handlers, explicit checks & handlers, and compatibility with existing code. That’s entirely too vague to motivate the proffered design.

I’ve posted a long list of possible error handling requirements.

1.check/handle doesn't offer multiple handler pathways. It’s common for a function to apply two or more kinds of recurring error handling, e.g.

{ log.Println(err); return err }
{ log.Fatal(err) }
{ if err == io.EOF { break } }
{ conn.Write([]byte("oops: " + err.Error())) } // network server

Any code that depends on OS, storage, and/or networking APIs deals with errors stemming from them differently than errors due to incorrect input or unexpected internal state.

This is such a glaring omission that over a third of the responses on the feedback wiki suggest various ways to select one of several named handlers.

2.The last-in-first-out (i.e. inverted) handle chain can only bail out of a function, i.e. it cannot admit recoverable errors. Huh? Many conditions that APIs report as errors aren’t actually exceptional. So now we shall wield both err!=nil and check. En garde!

handle err { ... }
v, err := f()
if err != nil { // got deja vu?
if isBad(err) {
check err // is that a throw?
}
// recovery code
}

Serious Q: Is there an error handling model in any other language which is solely dedicated to bail-out?

3.check is specific to type error and the last return value. And if we need to inspect other return values/types for exceptional state? Back to this:
v, errno := f(); if errno != 0 { ... }

4.Nesting of function calls that return errors, via check, obscures the order of operations. In most cases, the state of affairs when an error occurs, and therefore the sequence of calls, should be clear, but here it’s not:
check step4(check step1(), check step3(check step2()))
And nesting can foster unreadable constructions:
f1(v1, check f2(check f3(check f4(v4), v3), check f5(v5)))
Now recall that the language forbids: 
f(t ? a : b) and f(a++)

5.The Default Handler is too friendly. The goal of “raising the likelihood” that developers will write error handlers is thwarted by the default handler, which makes it trivial to return errors without context.

6.Error handlers performing cleanup appear before the code that causes an untidy state. If they followed the normal order, we’d see a code structure similar to the one we already know:

for ... {
check untidy()
}
check done()
handle err { cleanup(); return err }

On the bright side, code that hops backwards during bail-out is more stimulating to read! Try it yourself in the next item :-)

7.The handle chain is inscrutable. The steps taken on bail-out can be spread across a function and are not labeled, so you must parse the whole function by eye to unveil the handler sequence for any given error. For the following example, cover the comments column and see how it feels…

func f() error {
handle err { return ... } // finally this
if ... {
handle err { ... } // not that
for ... {
handle err { ... } // nor that
...
}
}
handle err { ... } // secondly this
...
if ... {
handle err { ... } // not that
...
} else {
handle err { ... } // firstly this
check thisFails() // trigger
}
}

This may be reasonable if the handlers only do context accretion:
handle err { err = fmt.Errorf("blurb: %v", err) }
But any handler can do anything, including return the function. That risk is another reason so many folks have asked for named handlers, so they can chain them explicitly, if at all.

There you have it; check/handle is a modest collection of flaws and missing features in service to a noble idea. I am nonetheless indebted to the Go team for discovering and developing the language that I rely on most!

I am not one to fuss about the quirks of a language. But I do tinker with code style to make my source more meaningful, which for Go means eschewing the go fmt command in order to write this and other one-line constructs:

if err != nil { return err } // or log.Fatal(err), etc

And that alone makes today’s Go 1 error handling more palatable than the check/handle concoction. ~~