Skip to content

Instantly share code, notes, and snippets.

@networkimprov
Last active March 10, 2024 22:08
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save networkimprov/961c9caa2631ad3b95413f7d44a2c98a to your computer and use it in GitHub Desktop.
Save networkimprov/961c9caa2631ad3b95413f7d44a2c98a to your computer and use it in GitHub Desktop.
Requirements to Consider for Go 2 Error Handling

Requirements to Consider for Go 2 Error Handling

Towards consensus on the requirements for a new errors idiom.

The Go2 Error Handling Overview gives only four rather vague "goals": small-footprint error checks, developer-friendly error handlers, explicit checks & handlers, and compatibility with existing code. It makes no mention of potential future extensions of the concept. Previously @ianlancetaylor made a similar list in Github issue #21161.

As of this writing, there are over thirty counter-proposals on the feedback wiki for the Go 2 Error Handling Draft Design. Almost none of them discuss the requirements that motivate them, but many imply requirements that the draft design does not fulfill. The Go team might benefit from a consensus on requirements before penning a next draft.

Below is a comprehensive menu of possible requirements (i.e. it's not all-or-nothing). Please comment with any others that should be considered.

PS: For those wondering why I've taken the trouble, it's because I had this reaction to the draft design :-)

This hit the front page of Hacker News in October 2018.

The Menu

A. Allow Go1 error idiom to continue working if err != nil { ... }

B. Readable, reusable error "handlers" within a function and/or package.

  1. Uniquely identify handlers (required by (C) below), e.g.

    handle h T { return h }     // id is parameter name
    handle h(p T) { return p }  // id is handler name
    h: handle p T { return p }  // id is label
    

    1.1 Let handler identity be visible anywhere within its enclosing scope.

  2. Let handler body contain any code that's valid in the enclosing function.

    2.1 Disallow "bare return" in a handler body, e.g.

    func f() (i int, e error) {
       ...
       handle h T { return }    // compile error
    }
    
  3. Let handler parameter be of any type, or only type error

    3.1 Perform implicit type assertion if parameter type implements an interface appearing in invocation of handler, e.g.

    func (t *T) Error() string { ... }  // T implements error
    func (t *T) x() { ... }
    ?h = func() error { return T{} }()  // invoke handler h with value type error
    handle h T { h.x() }                // implicit h = value.(T)
    
  4. Invoke handler body on any parameter value indicating error (typically non-zero).
    Or, for a slice parameter type, on any element value indicating error.

    4.1 Let a guard expression indicate the condition in which to run the handler body, e.g.

    handle h string; h == "failure" { ... }
    
  5. Permit handlers for deferred function calls, e.g.
    defer handle h T { return h }

C. Clear, concise way to select a handler by name (17 posts on the feedback wiki suggest this). Prevent the dissociation between function calls and handler blocks that occurs in the try/catch model.

  1. Let assignment invoke a handler (13 posts on the feedback wiki suggest this).

  2. Let function call invoke a handler.

// op: keyword or symbol (e.g. ? @ # and unambiguous pairings)
//     space between symbol and identifier is not required
// hname: handler name

v, hname op := f(a)   // assignment
v, op hname := f(a)

v := op(f(a), hname)  // function call
v := op(hname, f(a))
v := f(a) op hname
v := hname op f(a)
v := f op hname (a) // n.b.

D. Minimize boilerplate and name proliferation.

  1. Infer handler name and type from previous statement, e.g.

    op hname = f()
    handle { return fmt.Errorf(..., hname) }
    
  2. Let name of unreachable handler be reused, e.g.

    op hname = f(1)
    handle hname T { ... }
    op hname = f(2)
    handle hname T { ... }
    

E. Let function returning error and a value serve as expression, e.g.

f := func(i int) (T, error) { ... }
x(f op hname (a))
f op hname (a).x()
handle hname error { ... }
  1. Let return value passed to handler default to the last one.

  2. Let index of return value passed to handler be specified, e.g.
    x(f op hname : digit (a))

  3. Disallow expression syntax for assignment, e.g.
    v = f op hname (a) // compile error

F. Invoke handler on boolean false parameter for

v, op hname := m[k]
v, op hname := x.(T)
v, op hname := <-c
  1. Generate meaningful parameter value, e.g.
    v, op hname := m[k]
    handle hname error { return hname }
    

G. Let handling of an error skip over any statements between the statement triggering the handler and the handler body. (Entails placement of handler after statements that invoke it.) e.g.

op hname = f()
for {
   if x() { op hname = T{} }
}
handle hname T { ... }
  1. Let handling of an error from a deferred function call skip over any defer statements between the trigger and handler body, e.g.

    defer handle hname T { ... } // triggered by f()
    defer skipOnError()
    defer f op hname ()
    
  2. Implicitly set any variables declared between the first invocation of a handler and the handler body to their zero value, if they are read within the handler, e.g.

    op hname = f(1)
    v := 2
    op hname = f(v)
    handle hname T {
       fmt.Println(v) // always 0
    }
    

    2.1 Alternatively disallow use of such variables within the handler without a preceding assignment in-handler.

H. Let handler continue the function. (Entails placement of handler after statements that invoke it.) e.g.

for {
   op hname = f()
   handle hname T {
      if ... { continue }
      break
   }
   x()
}
  1. Let a special statement exit the handler, e.g.
    handle hname T { if ... { break op hname } }

  2. Let a handler invoked by a deferred function call continue up the defer stack, e.g.

    defer runLast()
    defer handle hname T { ... } // triggered by f()
    defer f op hname ()
    

I. Let handler invoke another handler explicitly, e.g.

handle err error {
   if err != io.EOF {
      op quit = err     // invoke "quit" handler
   }
}
...
handle quit T { ... }
  1. Let the invocation skip the nil check by making the handle parameter const.
    handle err error { err = fmt.Errorf(...) } // compile error

  2. Let handler invoke another handler implicitly, e.g. handlers with same name in related scopes (not recommended).

J. Let handler perform context accretion, e.g.

handle err error { op quit = fmt.Errorf("blurb: %v", err) }
  1. Provide invocation site metadata to handler, e.g. line number, called function (if any), parent function, source file.

K. Permit package-level handlers, essentially a custom "predefined handler", e.g.

package main
func f() {
   op hname = x()
}
handle hname T { ... }
  1. Provide caller name to handler, e.g.
    handle (hname T, caller string) { ... }

  2. Let handling of an error skip over any statements preceding a package-level handler reference, e.g.

    func f() {
       op hname = x()
       skipOnError()
       handle hname   // handler reference
       ...
    }
    handle hname T { ... }
    

L. Provide predefined handlers, e.g.

op _      = f() // ignore error
op        = f() // for f?()

op panic  = f() // panic on error
op p      + f() // for f?p(); or any substring of "panic"
op !      = f() // for f?!(); similar to "_" but perhaps cryptic

op return = f() // return on error; may discourage returning error with added context :-/
op r      = f() // for f?r(); or any substring of "return"
op .      = f() // for f?.(); similar to "_" but perhaps cryptic
  1. Let the ignore handler log errors or all values in a debug mode.

  2. Disallow the return handler in functions which don't define the requisite type as the last return value.

    2.1. Goroutine functions and main.main shall not return an error, or not any values.

M. Calls to functions returning type error shall invoke a handler for the error value, or assign it to a variable (i.e. not _).

op _ = f() // OK
 err = f() // OK
   _ = f() // compile error
       f() // compile error

N. Define keywords for handle and possibly op above. Consider any, or only C-family, or only Go1 keywords, e.g.

handle hname T { ... }
catch hname T { ... }
func op hname T { ... }

O. Let automated test procedures trigger handlers in functions being tested.

  1. Let the test tooling generate a variant of the function to test, with its handler invocations altered, e.g.
    func f(i int) {                                    // function to test
       op hname = x(i)
       handle hname T { ... }
    }
    
    // generate one of the following, as needed by test procedure
    
    func f_errors(i int, e ...interface{}) {           // argument per handler invocation
       op hname = e[0].(T)                             // or e[0].(func(int)T)(i)
       handle hname T { ... }
    }
    
    func f_errors(i int, e map[string][]interface{}) { // map of handler names, slice item per invocation
       if len(e[hname]) < 1 {
          op hname = x(i)
       } else {
          op hname = e[hname][0].(T)                   // or e[hname][0].(func(int)T)(i)
       }
       handle hname T { ... }
    }
    
@networkimprov
Copy link
Author

Apology for the slow response; been working on software releases lately...

I'd encourage you to post a link to your document on https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

I'll also review and try to incorporate your points into the above Requirements menu when I have a chance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment