Now posted on the golang issue tracker, #27519.
Appreciations to the Go team for presenting a thoughtful and detailed set of Go2 draft designs. The Error Handling Draft is great for logging errors via the handler chain, and returning them succinctly. However, Go programs commonly:
a) handle an error and continue the function that received it, and
b) have two or more kinds of recurring error handling in a single function, such as:
{ log.Println(err.Error()); return err }
{ log.Fatal(err) }
{ if err == io.EOF { break } }
{ conn.Write([]byte("oops: " + err.Error())) } // e.g. a network message processor
The check/handle
scheme doesn't accommodate these patterns, necessitating an awkward mix of Go1 & Go2 idioms:
if err = f(); err != nil {
if isSerious(err) { check err }
// handle recoverable error
}
And there are other problems, outlined in Why Not check/handle below.
func (db *Db) GetX(data []byte) (int, error) {
n, #_ := db.name() // return our own errors via default handler
f, #err := os.Open(n) // this file's presence is optional
defer f.Close()
_, #err = f.Seek(42, io.SeekStart)
l, #err := f.Read(data)
#_ = db.process(data)
catch err error { // handle OS errors here
if !os.IsNotExist(err) { log.Fatal(err) }
log.Println(n, "not found; proceding")
#err = nil // resume (should be the default?)
}
return l, nil
}
Here a catch identifier (catch-id) e.g. #err
selects an error handler.
A single catch-id may appear in any assignment.
A handler is known by its parameter name; the parameter can be of any type.
Handlers with the same parameter name in related scopes form a chain.
A handler follows the catch-id(s) that trigger it and starts with keyword catch
.
Catch-ids are not variables and handler parameters are only visible within handlers,
so there's no re-declaration of error variables.
Several points are unresolved, see Open Questions below.
Catch-id syntax is among them; #id
is reminiscent of the URL form for goto id, but ?id
, @id
, and others are viable.
Please help clarify (or fix) this proposal sketch, and describe your use cases for its features.
We can select one of several distinct handlers:
func f() error {
v1, #fat := fatalIfError() // a non-zero value for #id triggers the corresponding catch
v2, #wrt := writeIfError()
v3, #_ := returnIfError() // default handler, returns the value if triggered
v4, #! := panicIfError() // default handler, panics if triggered; for callers that don't return
catch fat error { log.Fatal(fat) } // if type is error, type name could be optional
catch wrt error { con.Write(...); return nil } // return/exit required in last handler on chain
}
We can chain handlers across scopes, as in the Error Handling Draft:
func f() {
v, #fat := x()
if v != nice { // new scope
#fat = y(&v)
catch fat error { debug.PrintStack() } // no return/exit; chained with next catch fat
}
catch fat error { log.Fatal(fat) } // parameter types must be the same across the chain
}
We can invoke a handler defined at package level (thanks @8lall0):
func f() error {
#pkg = x()
catch pkg { ... } // optional
}
catch pkg { // package-level handler
log.Println(pkg.Error())
return pkg // return signature must match function invoking pkg handler
}
We can cancel the chain and resume after the handler:
#err = f()
catch err { log.Println(...); #err = nil } // exit handler; don't goto next handler in chain
// can be the only/last handler in chain
x() // called after handler
We can change the input for the next chained handler:
if t {
#err = f()
catch err {
if ... {
#err = MyError{...} // exit handler; set input for next handler in chain
} else {
err = nil // no effect on next handler
}
}
}
catch err { return err } // if missing, compiler complains about self-invocation
We can forward to a different handler chain (can be applied to create an explicit chain in lieu of an implicit one):
if ... {
#err = f()
catch err { if ... { #ret = err } } // exit handler; pass err to handler in same or outer scope
// err must be non-zero
}
catch ret { ... }
catch err { ... } // may not be called
We can see everything from the scope where a handler is defined, like closure functions:
v1 := 1
if t {
v2 := 2
#err = f()
catch err { x(v1, v2) }
}
We can still use Go1 error handling:
v1, err := x() // OK
v2, err := y() // but re-declaration might be abolished!
- What catch-id syntax?
#id
,?id
,@id
,id!
,$id
, ... - What catch-id for default handlers?
#_
,#!
,#default
,#panic
, ... - What handler definition syntax?
catch id
,catch id type
,catch (id type)
,except id
, ... - Require
#id
or_
for return values of typeerror
? - Provide
check
functionality withf#id()
? e.g.x(f1#_(), f2#err())
If so, disallow nesting?x(f1#err(f2#err()))
- Allow unused handler?
func f() { catch ret { ... } // unused #ret = x() catch ret { ... } // OK; preceding catch-id selects this catch err { ... } // unused }
- Drop implicit handler chaining? (Handler default action would be resume.)
if ... { #err = f1() catch err { log.Println(...) } } #err = f2() catch err { return nil } // sees errors from f1 & f2; was f2 the only intended source?
- Provide more context to package-level handlers, e.g. caller name, arguments?
catch pkg, caller { log.Println(caller, pkg.Error()) }
- Allow multiple handler arguments?
#val, #err = f() // return values must be assignable to catch parameter types catch val T, err error { ... } // either parameter could be non-zero
Reading a catch-id:
#err = f()
if #err != nil { ... } // compiler complains
catch err { ... }
Multiple catch-ids per statement:
#val, #err = f() // compiler complains
catch val { ... } // if f() returns two non-zero values, which handler is executed?
catch err { ... }
Shadowing of local variables in handlers:
func f() {
if t {
err := 2
#err = f() // OK; #err handler can't see this scope
}
pkg := 1 // OK; #pkg handler (see above) can't see local variables
err := 1
#err = f()
catch err { return err } // compiler complains; err==1 is shadowed
}
Contiguous handlers with the same catch-id and scope:
#err = f()
catch err { ... }
#ret = f()
catch err { return err } // compiler complains
catch ret { ... }
catch ret { return ret } // compiler complains
check
is specific to typeerror
and the last return value.check
doesn't support multiple distinct handlers.- Function call nesting with a per-call unary operator can foster unreadable constructions:
f1(v1, check f2(check f3(check f4(v4), v3), check f5(v5)))
May I remind you, we are forbidden this:f(t ? a : b)
- The outside-in
handle
chain can only bail out of a function. - Handlers that add context should appear after the relevant calls, in the order of operations:
for ... { #err = f() catch err { #err = fmt.Errorf("loop: %s", err.Error()) } } catch err { return fmt.Errorf("context: %s", err.Error()) }
- There is relatively little support for the draft design on the feedback wiki.
At last count, more than 1/3rd of posts on the feedback wiki suggest ways to select one of several handlers:
- @didenko github
- @forstmeier gist
- @mcluseau gist
- @the-gigi gist
- @PeterRK gist
- @marlonche gist
- @alnkapa github
- @pdk medium
- @gregwebs gist
- @gooid github
- @networkimprov this page
/cc @rsc @mpvl @griesemer @ianlancetaylor @8lall0 @sdwarwick @kalexmills
@gopherbot add Go2 LanguageChange Proposal
Thanks for your consideration,
Liam Breck
Menlo Park, CA, USA
very much agree that multiple handlers should be possible.
another aspect not being discussed is testing - what syntax would make a testing harness easiest?