Tuesday 2 October 2018

Go Error Handling – Using Closures

The Go 2 Error handling draft design was released recently (see Error Handling Overview). I took great interest in it as it is similar to my own proposal made last year (see Improving Go Error Handling) but goes a step further by adding "handlers".

I like the reasoning behind handlers but I believe something better is needed. Handlers have been discussed at length in the feedback page (see wiki feedback page) so I won't add to that noise. [If you have not read any of the other feedback then basically people find handlers confusing - both conceptually and in use, especially if chained.]

Proposal – using Closures 

Instead of little bits of code in handle statements why not simply use the existing Go facility of closures. A closure could be provided for error-handling of a specific expression by putting its name in brackets after the checked keyword.  All such error-handling closures would take one (error) parameter and return an error value.

As in the original draft design, a "default" handler is provided. This is used if the check keyword does not specify a closure.  The "default" handler would be simply implemented as:

func defaultHandler(err error) error { return err }

The advantages to this are:
  • it is clearer about where an error for any specific expression is handled 
  • handler "chaining" is still possible  (see example below)
  • closures make chaining more obvious
  • closures provide even more flexibility than handlers/chaining
  • error-handling code can be easily separated so as not to obscure the code 
  • but it is still easy to trace how the error is handled if necessary
  • error handling code can be moved outside the function and even re-used 
Here is the CopyFile() example from the Error Handling Overview done in this way:

func CopyFile(src, dst string) error {
  eMess := func(err error) error {
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
  }

  r := check(eMess) os.Open(src)
  defer r.Close()
  w := check(eMess) os.Create(dst)

  eClose := func(err error) err {
    w.Close()
    os.Remove(dst)
    return eMess(err)
  }

  check(eClose) io.Copy(w, r)
  check(eClose) w.Close()
  return nil
}

Further Proposal – Check Statement 

 A further idea is to also allow check to work as a statement. Enclosing statements in a check statement would allow the same check operation to be applied to any contained function calls that returned an error as the last returned value.  Using the same example this could be used this way:

func CopyFile(src, dst string) error {
  eMess := func(err error) error {
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
  }
  check(eMess) {
    r := os.Open(src)
    defer r.Close()
    w := os.Create(dst)
  }

  eClose := func(err error) err {
    w.Close()
    os.Remove(dst)
    return eMess(err)
  }
  check(eClose) {
    io.Copy(w, r)
    w.Close()
  }
  return nil
}

Separating Error Handling Code 

 Note that the closure used with the check statement could be an expression evaluating to a function (just as happens with the defer statement).  That is, it could be a function call returning a closure.  This allows all the error-handling code to be hived off to another function where it is less distracting.

func CopyFile(src, dst string) error {
  var w *File
  check(genErr(src, dst, w)) {
    r := os.Open(src)
    defer r.Close()
    w = os.Create(dst)
    io.Copy(w, r)
    w.Close()
    w = nil
  }
  return nil
}

func genErr(src, dst string, w *os.File) func(err error ) error {
  return func(err error ) error {
    if w != nil {
      // output file was opened so close and remove it
      w.Close()
      os.Remove(dst)
    }
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
  }
}

Summary

Using closures instead of adding the new feature of handlers fits better with how Go currently works and makes use of the Go's wonderful feature of closures.

I'd like to thank Rob Pike for speaking at the Sydney Go Meetup last Thursday. His talk inspired me to think further about Go error-handling and write this post. Go is by far the least annoying language I have ever used and hopefully the addition of an improved error handling facility will make it even less so.

1 comment:

  1. Liked your proposal, but the last example (function call returning a closure) isn't helping, it's just unnecessarily complex.

    ReplyDelete