The Go 'rolling errors' pattern, in function call form

September 22, 2015

One of the small annoyances of Go's explicit error returns is that the basic approach of checking error returns at every step is annoying when all the error handling is actually the same. You wind up with the classic annoying pattern of, say:

s.f1, err = strconv.ParseUint(fields[1], 10, 64)
if err != nil {
   return nil, err
}
s.f2, err = strconv.ParseUint(fields[2], 10, 64)
if err != nil {
   return nil, err
}
[... repeat ...]

Of course, any good lazy programmer who is put into this starting situation is going to come up with a way to aggregate that error handling together. Go programmers are no exception, which has led to what I'll call a generic 'rolling errors' set of patterns. The basic pattern, as laid out in Rob Pike's Go blog entry Errors are values, is that as you do a sequence of operations you keep an internal marker of whether errors have occurred; at the end of processing, you check it and handle any error then.

Rob Pike's examples all use auxiliary storage for this internal marker (in one example, in a closure). I'm a lazy person so I tend to externalize this auxiliary storage as an extra function argument, which makes the whole thing look like this:

func getInt(field string, e error) (uint64, error) {
   i, err := strconv.ParseUint(field, 10, 64)
   if err != nil {
      return i, err
   }
   return i, e
}

func .... {
   [...]

   var err error
   s.f1, err = getInt(fields[1], err)
   s.f2, err = getInt(fields[2], err)
   s.f3, err = getInt(fields[3], err)

   if err != nil {
      return nil, err
   }
   [...]
}

This example code does bring up something you may want to think about in 'rolling errors' handling, which is what operations you want to do once you hit an error and which error you want to return. Sometimes the answer is clearly 'stop doing operations and return the first error'; other times, as with this code, you may decide that any of the errors is okay to return and it's simpler if the code keeps on doing operations (it may even be better).

(In retrospect I could have made this code just as simple while still stopping on the first error, but it didn't occur to me when I put this into a real program. In this case these error conditions are never expected to happen, since I'm parsing what should be numeric fields that are in a system generated file.)

As an obvious corollary, this 'rolling errors' pattern doesn't require using error itself. You can use it with any running or accumulated status indicator, including a simple boolean.

(Sometimes you don't need the entire infrastructure of error to signal problems. If this seems crazy, consider the case of subtracting two accumulating counters from each other to get a delta over a time interval where a counter might roll over and make this delta invalid. You generally don't need details or an error message here, you just want to know if the counter rolled over or not and thus whether or not you want to disregard this delta.)


Comments on this page:

By Ewen McNeill at 2015-09-22 01:58:50:

In some languages I've used the pattern:

all_ok = true
if (all_ok):
    ....
if (all_ok):
    ....
if (all_ok):
    ....

if (not all_ok):
    # Do error handling here

and it seems to me that'd also work for the fall through error handling case, if you wanted stop-at-first-error. "all_ok"'s sense can of course be inverted, and, eg, become "err != nil". The above pattern is basically a while loop style of continuing, but applied to a sequence of things.

Verus your second example which seems to be try-to-do-everything-ignoring-errors -- then figure out if it all worked at the end. Which could be rewritten:

s.f1, e1 = getInt(fields[1])
s.f2, e2 = getInt(fields[2])
s.f3, e3 = getInt(fields[3])

if (e1 != nil or e2 != nil or e3 != nil) {
   ...
}

(ie, the main saving of making the called function/functor store pass along the error is to avoid creating lots of "eN" variables to check at the end -- which is definitely a good thing to avoid, but forcing that tracking onto each called function feels a bit like make work. Maybe with a decorator...)

FWIW, Rob Pike's ones seem to "do nothing" inside the called function after an error is encountered. Sort of the "make free(NULL) safe" style handling of always checking for errors and shortcutting out at the top of each function.

Also FWIW, your version would seem to leave the error handling reporting the last error found, rather than the first error found. Because if it all keeps going wrong, the first error found is overwritten by the next error found... which may not matter if you just want to report "sorry, it didn't work out".

Ewen

Written on 22 September 2015.
« When chroot() started to confine processes inside the new root
One thing I'm hoping for in our third generation fileservers »

Page tools: View Source, View Normal, Add Comment.
Search:
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Tue Sep 22 00:23:46 2015
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.