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.
A. Allow Go1 error idiom to continue working if err != nil { ... }
B. Readable, reusable error "handlers" within a function and/or package.
-
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.
-
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 }
-
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)
-
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" { ... }
-
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.
-
Let assignment invoke a handler (13 posts on the feedback wiki suggest this).
-
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.
-
Infer handler name and type from previous statement, e.g.
op hname = f() handle { return fmt.Errorf(..., hname) }
-
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 { ... }
-
Let return value passed to handler default to the last one.
-
Let index of return value passed to handler be specified, e.g.
x(f op hname : digit (a))
-
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
- 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 { ... }
-
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 ()
-
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()
}
-
Let a special statement exit the handler, e.g.
handle hname T { if ... { break op hname } }
-
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 { ... }
-
Let the invocation skip the nil check by making the handle parameter
const
.
handle err error { err = fmt.Errorf(...) } // compile error
-
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) }
- 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 { ... }
-
Provide caller name to handler, e.g.
handle (hname T, caller string) { ... }
-
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
-
Let the ignore handler log errors or all values in a debug mode.
-
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.
- 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 { ... } }
I think this is covered by B-2 "Let handler body contain any code that's valid in the enclosing function" and/or H "Let handler continue the function".