2015-06-08
Exceptions as aggregators of error handling
As is becoming traditional, I'll start with the tweets:
@jaqx0r: People complaining about "if err != nil" density in #golang don't check errors in other languages. Discuss.
@thatcks: In exception-based languages, you often get to aggregate a bunch of similar/same error handling together in one code bit.
As usual on Twitter, this is drastically crunched down to fit into 140 characters and thus not necessarily all that clear. To make it clear, I'll start with a really simplistic example of error checking in a routine in Go:
func (c *MyConn) reply(code int, msg string) error { emsg, e := utils.encode(msg) if e != nil { return e } e = c.writecode(code) if e != nil { return e } e = c.writemsg(emsg) if e != nil { return e } e = c.logger.logreply(code, msg) return e }
If it runs into an error, all that this code does is abort further processing and returns the error to the caller. There is a slightly less verbose way to write this but regardless of how you write it, you have to explicitly check errors each time even though you're handling them all the same way.
In an exception-based language like Python we can write the code so that all of these 'if error, abort' checks are aggregated together. In a slightly more elaborate example we might write something like:
def reply(self, code, msg): try: emsg = utils.encode(msg) self.writecode(code) self.writemsg(emsg) self.logger.logreply(code, msg) except (A, B, C) as exp: ... cleanup ... raise MyException(exp)
Since the action taken on errors is the same for all cases, we've been able to aggregate it together into a single exception handling block with a single set of code. This clearly results in less code here.
The more that your code wants to do basically the same thing on errors (perhaps with some variations in messages or the like), the more you will benefit from error aggregation through exceptions and the more onerous and annoying it is to repeat yourself every single time. Conversely, the more different your error handling is, the less you benefit; in fact in some environments the exception-based version can wind up more verbose. My perception is that often you have essentially the same handling of a lot of errors and so the aggregation offered by exceptions will wind up saving you code and repetition.
(There are tricks to avoid repeating yourself with explicit error checking but at least some of them wind up distorting the flow of your code, for example by inserting an intermediary function whose only real purpose is to aggregate the error handling actions into one place.)
So, I think that people who complain about the density of explicit error checks in Go (and similar languages) have a real case here. I don't think Go will ever solve it, but these people are not necessarily writing sloppy code in other languages, just efficiently structured code.
(This is basically what I wrote back in Exceptions as efficient programming.)
Sidebar: Some pragmatic arguments for the Go way
There are arguments about code readability here, of course. The Go version makes it explicit that the only action done on error each time is to return immediately from the function, while the Python version moves that to a separate block of code. It's not very distant in this contrived version, but you can imagine a more realistic one where there was a substantial amount of code that was being wrapped inside a single exception handler (thus moving the two much further apart). As a structural matter, the Go example also clearly handles all error cases that can arise from each function call, while in the Python one we've assumed that we know all of the exceptions that can be raised underneath us.
Go's tradeoffs here strike me as the correct ones given Go's overall goals, even if I'm not sure I like them. Still, I can't say that handling explicit errors has been any particular pain in my Go code to date.